cuprum-collections 0.4.0 → 0.5.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -0
  3. data/README.md +5 -5
  4. data/lib/cuprum/collections/association.rb +9 -28
  5. data/lib/cuprum/collections/associations/belongs_to.rb +1 -8
  6. data/lib/cuprum/collections/associations/has_many.rb +1 -10
  7. data/lib/cuprum/collections/associations/has_one.rb +1 -10
  8. data/lib/cuprum/collections/basic/collection.rb +56 -49
  9. data/lib/cuprum/collections/basic/command.rb +22 -88
  10. data/lib/cuprum/collections/basic/commands/assign_one.rb +2 -6
  11. data/lib/cuprum/collections/basic/commands/build_one.rb +1 -4
  12. data/lib/cuprum/collections/basic/commands/destroy_one.rb +4 -8
  13. data/lib/cuprum/collections/basic/commands/find_many.rb +4 -24
  14. data/lib/cuprum/collections/basic/commands/find_matching.rb +5 -21
  15. data/lib/cuprum/collections/basic/commands/find_one.rb +3 -20
  16. data/lib/cuprum/collections/basic/commands/insert_one.rb +3 -6
  17. data/lib/cuprum/collections/basic/commands/update_one.rb +3 -6
  18. data/lib/cuprum/collections/basic/commands/validate_one.rb +13 -18
  19. data/lib/cuprum/collections/basic/query.rb +26 -40
  20. data/lib/cuprum/collections/basic/repository.rb +4 -3
  21. data/lib/cuprum/collections/basic/scopes/all_scope.rb +25 -0
  22. data/lib/cuprum/collections/basic/scopes/base.rb +32 -0
  23. data/lib/cuprum/collections/basic/scopes/builder.rb +39 -0
  24. data/lib/cuprum/collections/basic/scopes/conjunction_scope.rb +20 -0
  25. data/lib/cuprum/collections/basic/scopes/criteria_scope.rb +62 -0
  26. data/lib/cuprum/collections/basic/scopes/disjunction_scope.rb +20 -0
  27. data/lib/cuprum/collections/basic/scopes/none_scope.rb +33 -0
  28. data/lib/cuprum/collections/basic/scopes.rb +23 -0
  29. data/lib/cuprum/collections/basic.rb +1 -0
  30. data/lib/cuprum/collections/collection.rb +24 -82
  31. data/lib/cuprum/collections/collection_command.rb +116 -0
  32. data/lib/cuprum/collections/commands/abstract_find_many.rb +11 -21
  33. data/lib/cuprum/collections/commands/abstract_find_matching.rb +43 -24
  34. data/lib/cuprum/collections/commands/abstract_find_one.rb +7 -10
  35. data/lib/cuprum/collections/commands/associations/find_many.rb +3 -8
  36. data/lib/cuprum/collections/commands/associations/require_many.rb +5 -5
  37. data/lib/cuprum/collections/commands/create.rb +3 -3
  38. data/lib/cuprum/collections/commands/find_one_matching.rb +6 -6
  39. data/lib/cuprum/collections/commands/query_command.rb +19 -0
  40. data/lib/cuprum/collections/commands/update.rb +3 -3
  41. data/lib/cuprum/collections/commands/upsert.rb +10 -10
  42. data/lib/cuprum/collections/commands.rb +1 -0
  43. data/lib/cuprum/collections/constraints/ordering.rb +2 -2
  44. data/lib/cuprum/collections/errors/abstract_find_error.rb +25 -42
  45. data/lib/cuprum/collections/errors/extra_attributes.rb +3 -3
  46. data/lib/cuprum/collections/errors/failed_validation.rb +2 -2
  47. data/lib/cuprum/collections/errors/invalid_parameters.rb +2 -2
  48. data/lib/cuprum/collections/errors/invalid_query.rb +10 -16
  49. data/lib/cuprum/collections/errors/missing_default_contract.rb +1 -1
  50. data/lib/cuprum/collections/errors/unknown_operator.rb +1 -1
  51. data/lib/cuprum/collections/queries.rb +31 -0
  52. data/lib/cuprum/collections/query.rb +50 -62
  53. data/lib/cuprum/collections/relation.rb +5 -383
  54. data/lib/cuprum/collections/relations/cardinality.rb +66 -0
  55. data/lib/cuprum/collections/relations/options.rb +18 -0
  56. data/lib/cuprum/collections/relations/parameters.rb +217 -0
  57. data/lib/cuprum/collections/relations/primary_keys.rb +23 -0
  58. data/lib/cuprum/collections/relations/scope.rb +65 -0
  59. data/lib/cuprum/collections/relations.rb +14 -0
  60. data/lib/cuprum/collections/repository.rb +5 -5
  61. data/lib/cuprum/collections/resource.rb +10 -41
  62. data/lib/cuprum/collections/rspec/contracts/association_contracts.rb +80 -90
  63. data/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +69 -111
  64. data/lib/cuprum/collections/rspec/contracts/command_contracts.rb +42 -1335
  65. data/lib/cuprum/collections/rspec/contracts/query_contracts.rb +352 -531
  66. data/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +74 -191
  67. data/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +13 -13
  68. data/lib/cuprum/collections/rspec/contracts/scope_contracts.rb +1029 -0
  69. data/lib/cuprum/collections/rspec/contracts/scopes/builder_contracts.rb +856 -0
  70. data/lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb +1430 -0
  71. data/lib/cuprum/collections/rspec/contracts/scopes/criteria_contracts.rb +2217 -0
  72. data/lib/cuprum/collections/rspec/contracts/scopes/logical_contracts.rb +297 -0
  73. data/lib/cuprum/collections/rspec/contracts/scopes.rb +13 -0
  74. data/lib/cuprum/collections/rspec/contracts.rb +2 -0
  75. data/lib/cuprum/collections/rspec/deferred/association_examples.rb +2098 -0
  76. data/lib/cuprum/collections/rspec/deferred/collection_examples.rb +338 -0
  77. data/lib/cuprum/collections/rspec/deferred/command_examples.rb +160 -0
  78. data/lib/cuprum/collections/rspec/deferred/commands/assign_one_examples.rb +178 -0
  79. data/lib/cuprum/collections/rspec/deferred/commands/build_one_examples.rb +94 -0
  80. data/lib/cuprum/collections/rspec/deferred/commands/destroy_one_examples.rb +118 -0
  81. data/lib/cuprum/collections/rspec/deferred/commands/find_many_examples.rb +307 -0
  82. data/lib/cuprum/collections/rspec/deferred/commands/find_matching_examples.rb +143 -0
  83. data/lib/cuprum/collections/rspec/deferred/commands/find_one_examples.rb +116 -0
  84. data/lib/cuprum/collections/rspec/deferred/commands/insert_one_examples.rb +103 -0
  85. data/lib/cuprum/collections/rspec/deferred/commands/update_one_examples.rb +99 -0
  86. data/lib/cuprum/collections/rspec/deferred/commands/validate_one_examples.rb +117 -0
  87. data/lib/cuprum/collections/rspec/deferred/commands.rb +8 -0
  88. data/lib/cuprum/collections/rspec/deferred/relation_examples.rb +1437 -0
  89. data/lib/cuprum/collections/rspec/deferred/resource_examples.rb +26 -0
  90. data/lib/cuprum/collections/rspec/deferred.rb +8 -0
  91. data/lib/cuprum/collections/scope.rb +29 -0
  92. data/lib/cuprum/collections/scopes/all.rb +51 -0
  93. data/lib/cuprum/collections/scopes/all_scope.rb +18 -0
  94. data/lib/cuprum/collections/scopes/base.rb +79 -0
  95. data/lib/cuprum/collections/scopes/builder.rb +39 -0
  96. data/lib/cuprum/collections/scopes/building.rb +221 -0
  97. data/lib/cuprum/collections/scopes/composition.rb +162 -0
  98. data/lib/cuprum/collections/scopes/conjunction.rb +44 -0
  99. data/lib/cuprum/collections/scopes/conjunction_scope.rb +12 -0
  100. data/lib/cuprum/collections/scopes/container.rb +65 -0
  101. data/lib/cuprum/collections/scopes/criteria/parser.rb +241 -0
  102. data/lib/cuprum/collections/scopes/criteria.rb +206 -0
  103. data/lib/cuprum/collections/scopes/criteria_scope.rb +12 -0
  104. data/lib/cuprum/collections/scopes/disjunction.rb +45 -0
  105. data/lib/cuprum/collections/scopes/disjunction_scope.rb +12 -0
  106. data/lib/cuprum/collections/scopes/none.rb +62 -0
  107. data/lib/cuprum/collections/scopes/none_scope.rb +18 -0
  108. data/lib/cuprum/collections/scopes.rb +23 -0
  109. data/lib/cuprum/collections/version.rb +2 -2
  110. data/lib/cuprum/collections.rb +14 -9
  111. metadata +61 -15
  112. data/lib/cuprum/collections/basic/query_builder.rb +0 -69
  113. data/lib/cuprum/collections/command.rb +0 -26
  114. data/lib/cuprum/collections/queries/parse.rb +0 -22
  115. data/lib/cuprum/collections/queries/parse_block.rb +0 -206
  116. data/lib/cuprum/collections/queries/parse_strategy.rb +0 -91
  117. data/lib/cuprum/collections/query_builder.rb +0 -61
  118. data/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb +0 -484
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/deferred'
4
+
5
+ require 'cuprum/collections/rspec/deferred'
6
+ require 'cuprum/collections/rspec/deferred/relation_examples'
7
+
8
+ module Cuprum::Collections::RSpec::Deferred
9
+ # Deferred examples for testing resources.
10
+ module ResourceExamples
11
+ include RSpec::SleepingKingStudios::Deferred::Provider
12
+
13
+ deferred_examples 'should be a Resource' do
14
+ include Cuprum::Collections::RSpec::Deferred::RelationExamples
15
+
16
+ include_deferred 'should be a Relation',
17
+ cardinality: true
18
+
19
+ include_deferred 'should define Relation cardinality'
20
+
21
+ include_deferred 'should define Relation primary key'
22
+
23
+ include_deferred 'should define Relation scope'
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/rspec'
4
+
5
+ module Cuprum::Collections::RSpec
6
+ # Namespace for deferred example groups for validating collections.
7
+ module Deferred; end
8
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections'
4
+ require 'cuprum/collections/scopes/criteria_scope'
5
+
6
+ module Cuprum::Collections
7
+ # Generic scope class for defining collection-independent criteria scopes.
8
+ class Scope < Cuprum::Collections::Scopes::CriteriaScope
9
+ # @overload build(value = nil, &block)
10
+ # (see Cuprum::Collections::Scopes::Criteria::ClassMethods.build)
11
+ def self.build(...)
12
+ new(...)
13
+ end
14
+
15
+ # @overload initialize(value = nil, &block)
16
+ # @param value [Hash, nil] the keys and values to parse.
17
+ #
18
+ # @return [Array] the generated criteria.
19
+ #
20
+ # @yield the query block.
21
+ #
22
+ # @yieldreturn [Hash] a Hash with String keys.
23
+ def initialize(*args, inverted: false, &block)
24
+ criteria = self.class.parse(*args, &block)
25
+
26
+ super(criteria:, inverted:)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/scopes'
4
+
5
+ module Cuprum::Collections::Scopes
6
+ # Functionality for implementing an all scope, which returns all data.
7
+ module All
8
+ # @overload and(hash = nil, &block)
9
+ # Parses the hash or block and returns the parsed scope.
10
+ #
11
+ # @see Cuprum::Collections::Scopes::Criteria::Parser#parse.
12
+ #
13
+ # @overload and(scope)
14
+ # Returns the given scope.
15
+ def and(*args, &)
16
+ return self if scope?(args.first) && args.first.empty?
17
+
18
+ builder.build(*args, &)
19
+ end
20
+ alias where and
21
+
22
+ # @return [Boolean] false.
23
+ def empty?
24
+ false
25
+ end
26
+
27
+ # @return [Cuprum::Collections::Scopes::None] a none scope for the current
28
+ # collection.
29
+ def invert
30
+ builder.build_none_scope
31
+ end
32
+
33
+ # @overload or(hash = nil, &block)
34
+ # Parses the hash or block and returns the parsed scope.
35
+ #
36
+ # @see Cuprum::Collections::Scopes::Criteria::Parser#parse.
37
+ #
38
+ # @overload or(scope)
39
+ # Returns the given scope.
40
+ def or(*args, &)
41
+ return self if scope?(args.first) && args.first.empty?
42
+
43
+ builder.build(*args, &)
44
+ end
45
+
46
+ # (see Cuprum::Collections::Scopes::Base#type)
47
+ def type
48
+ :all
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/scopes'
4
+ require 'cuprum/collections/scopes/all'
5
+ require 'cuprum/collections/scopes/base'
6
+
7
+ module Cuprum::Collections::Scopes
8
+ # Generic scope class for defining collection-independent all scopes.
9
+ class AllScope < Cuprum::Collections::Scopes::Base
10
+ include Cuprum::Collections::Scopes::All
11
+
12
+ # @return [Cuprum::Collections::Scopes::AllScope] a cached instance of the
13
+ # all scope.
14
+ def self.instance
15
+ @instance ||= new
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/scopes'
4
+ require 'cuprum/collections/scopes/composition'
5
+
6
+ module Cuprum::Collections::Scopes
7
+ # Abstract class representing a set of filters for a query.
8
+ class Base
9
+ include Cuprum::Collections::Scopes::Composition
10
+
11
+ # Exception raised when inverting an uninvertible scope.
12
+ class UninvertibleScopeException < StandardError; end
13
+
14
+ def initialize(**); end
15
+
16
+ # @param other [Object] the object to compare.
17
+ #
18
+ # @return [Boolean] true if the other object is a scope with matching type;
19
+ # otherwise false.
20
+ def ==(other)
21
+ return false unless other.is_a?(Cuprum::Collections::Scopes::Base)
22
+
23
+ other.type == type
24
+ end
25
+
26
+ # @return [Hash{String=>Object}] a JSON-compatible representation of the
27
+ # scope.
28
+ def as_json
29
+ { 'type' => type }
30
+ end
31
+
32
+ # :nocov:
33
+
34
+ # @private
35
+ #
36
+ # Generates a string representation of the scope.
37
+ def debug
38
+ debug_class_name(self)
39
+ end
40
+ # :nocov:
41
+
42
+ # @return [Boolean] false.
43
+ def empty?
44
+ false
45
+ end
46
+
47
+ # Generates and returns an inverted copy of the scope.
48
+ #
49
+ # @raise [UninvertibleScopeException] if the scope does not implement
50
+ # #invert.
51
+ def invert
52
+ raise UninvertibleScopeException,
53
+ "Scope class #{self.class.name} does not implement #invert"
54
+ end
55
+
56
+ # @return [Symbol] the scope type.
57
+ def type
58
+ :abstract
59
+ end
60
+
61
+ private
62
+
63
+ def builder
64
+ Cuprum::Collections::Scopes::Builder.instance
65
+ end
66
+
67
+ # :nocov:
68
+ def debug_class_name(scope)
69
+ name = scope.class.name.sub(/\ACuprum::Collections::/, '')
70
+ segments =
71
+ name.split(/(::)?Scopes(::)?/).reject { |s| s.empty? || s == '::' }
72
+
73
+ segments.join('::')
74
+ end
75
+ # :nocov:
76
+ end
77
+ end
78
+
79
+ require 'cuprum/collections/scopes/builder'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/scopes'
4
+ require 'cuprum/collections/scopes/building'
5
+
6
+ module Cuprum::Collections::Scopes
7
+ # Builder for generating generic scopes.
8
+ class Builder
9
+ include Cuprum::Collections::Scopes::Building
10
+
11
+ private
12
+
13
+ def all_scope_class
14
+ Cuprum::Collections::Scopes::AllScope
15
+ end
16
+
17
+ def conjunction_scope_class
18
+ Cuprum::Collections::Scopes::ConjunctionScope
19
+ end
20
+
21
+ def criteria_scope_class
22
+ Cuprum::Collections::Scopes::CriteriaScope
23
+ end
24
+
25
+ def disjunction_scope_class
26
+ Cuprum::Collections::Scopes::DisjunctionScope
27
+ end
28
+
29
+ def none_scope_class
30
+ Cuprum::Collections::Scopes::NoneScope
31
+ end
32
+ end
33
+ end
34
+
35
+ require 'cuprum/collections/scopes/all_scope'
36
+ require 'cuprum/collections/scopes/conjunction_scope'
37
+ require 'cuprum/collections/scopes/criteria_scope'
38
+ require 'cuprum/collections/scopes/disjunction_scope'
39
+ require 'cuprum/collections/scopes/none_scope'
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/scopes'
4
+ require 'cuprum/collections/scopes/criteria'
5
+
6
+ module Cuprum::Collections::Scopes
7
+ # Abstraction for generating scopes for a given collection.
8
+ module Building # rubocop:disable Metrics/ModuleLength
9
+ # Error raised when trying to call an abstract builder method.
10
+ class AbstractBuilderError < StandardError; end
11
+
12
+ # Error raised when trying to transform an unknown scope type.
13
+ class UnknownScopeTypeError < StandardError; end
14
+
15
+ # Class methods to extend when including the module.
16
+ module ClassMethods
17
+ # @return [Cuprum::Collections::Scopes::Builder] a singleton instance of
18
+ # the builder class.
19
+ def instance
20
+ @instance ||= new
21
+ end
22
+ end
23
+
24
+ class << self
25
+ private
26
+
27
+ def included(other)
28
+ super
29
+
30
+ other.extend(ClassMethods)
31
+ end
32
+ end
33
+
34
+ # @overload build(hash = nil, &block)
35
+ # Parses the hash or block and returns a criteria scope.
36
+ #
37
+ # @see Cuprum::Collections::Scopes::Criteria::Parser#parse.
38
+ #
39
+ # @overload build(scope)
40
+ # Returns a new scope with the same scope type and properties.
41
+ def build(*args, &)
42
+ if args.first.is_a?(Cuprum::Collections::Scopes::Base)
43
+ return transform_scope(scope: args.first)
44
+ end
45
+
46
+ criteria_scope_class.build(*args, &)
47
+ end
48
+
49
+ # Creates a new all scope.
50
+ def build_all_scope
51
+ all_scope_class.new
52
+ end
53
+
54
+ # Creates a new logical AND scope wrapping the given scopes.
55
+ #
56
+ # @param scopes [Array<Cuprum::Collections::Scopes::Base>] the scopes to
57
+ # wrap in an AND scope.
58
+ # @param safe [Boolean] if true, validates and converts the scopes to match
59
+ # the builder's scope classes. Defaults to true.
60
+ def build_conjunction_scope(scopes:, safe: true)
61
+ if safe
62
+ validate_scopes!(scopes)
63
+
64
+ scopes = transform_scopes(scopes)
65
+ end
66
+
67
+ conjunction_scope_class.new(scopes:)
68
+ end
69
+
70
+ # Creates a new scope wrapping the given criteria.
71
+ #
72
+ # @param criteria [Array] the criteria for the scope.
73
+ # @param inverted [Boolean] true if the criteria scope is inverted.
74
+ # @param safe [Boolean] if true, validates the criteria. Defaults to true.
75
+ def build_criteria_scope(criteria:, inverted: false, safe: true)
76
+ validate_criteria!(criteria) if safe
77
+
78
+ criteria_scope_class.new(criteria:, inverted:)
79
+ end
80
+
81
+ # Creates a new logical OR scope wrapping the given scopes.
82
+ #
83
+ # @param scopes [Array<Cuprum::Collections::Scopes::Base>] the scopes to
84
+ # wrap in an AND scope.
85
+ # @param safe [Boolean] if true, validates and converts the scopes to match
86
+ # the builder's scope classes. Defaults to true.
87
+ def build_disjunction_scope(scopes:, safe: true)
88
+ if safe
89
+ validate_scopes!(scopes)
90
+
91
+ scopes = transform_scopes(scopes)
92
+ end
93
+
94
+ disjunction_scope_class.new(scopes:)
95
+ end
96
+
97
+ # Creates a new none scope.
98
+ def build_none_scope
99
+ none_scope_class.new
100
+ end
101
+
102
+ # Creates a new scope with the same scope type and properties.
103
+ def transform_scope(scope:)
104
+ validate_scope!(scope)
105
+
106
+ build_transformed_scope(scope)
107
+ end
108
+
109
+ private
110
+
111
+ def all_scope_class
112
+ raise AbstractBuilderError,
113
+ "#{self.class.name} is an abstract class. Define a builder " \
114
+ 'class and implement the #all_scope_class method.',
115
+ caller(1..-1)
116
+ end
117
+
118
+ def build_transformed_scope(original) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
119
+ case original.type
120
+ when :all
121
+ return original if original.is_a?(all_scope_class)
122
+
123
+ build_all_scope
124
+ when :conjunction
125
+ return original if original.is_a?(conjunction_scope_class)
126
+
127
+ build_conjunction_scope(
128
+ safe: false,
129
+ scopes: transform_scopes(original.scopes)
130
+ )
131
+ when :criteria
132
+ return original if original.is_a?(criteria_scope_class)
133
+
134
+ build_criteria_scope(
135
+ criteria: original.criteria,
136
+ inverted: original.inverted?,
137
+ safe: false
138
+ )
139
+ when :disjunction
140
+ return original if original.is_a?(disjunction_scope_class)
141
+
142
+ build_disjunction_scope(
143
+ safe: false,
144
+ scopes: transform_scopes(original.scopes)
145
+ )
146
+ when :none
147
+ return original if original.is_a?(none_scope_class)
148
+
149
+ build_none_scope
150
+ else
151
+ error_message =
152
+ "#{self.class.name} cannot transform scopes of type " \
153
+ "#{original.type.inspect} (#{original.class.name})"
154
+
155
+ raise UnknownScopeTypeError, error_message
156
+ end
157
+ end
158
+
159
+ def conjunction_scope_class
160
+ raise AbstractBuilderError,
161
+ "#{self.class.name} is an abstract class. Define a builder " \
162
+ 'class and implement the #conjunction_scope_class method.',
163
+ caller(1..-1)
164
+ end
165
+
166
+ def criteria_scope_class
167
+ raise AbstractBuilderError,
168
+ "#{self.class.name} is an abstract class. Define a builder " \
169
+ 'class and implement the #criteria_scope_class method.',
170
+ caller(1..-1)
171
+ end
172
+
173
+ def disjunction_scope_class
174
+ raise AbstractBuilderError,
175
+ "#{self.class.name} is an abstract class. Define a builder " \
176
+ 'class and implement the #disjunction_scope_class method.',
177
+ caller(1..-1)
178
+ end
179
+
180
+ def none_scope_class
181
+ raise AbstractBuilderError,
182
+ "#{self.class.name} is an abstract class. Define a builder " \
183
+ 'class and implement the #none_scope_class method.',
184
+ caller(1..-1)
185
+ end
186
+
187
+ def transform_scopes(scopes)
188
+ scopes.map { |scope| build_transformed_scope(scope) }
189
+ end
190
+
191
+ def validate_criteria!(criteria)
192
+ unless criteria.is_a?(Array)
193
+ raise ArgumentError, 'criteria must be an Array', caller(1..-1)
194
+ end
195
+
196
+ return if criteria.all? do |criterion|
197
+ criterion.is_a?(Array) && criterion.size == 3
198
+ end
199
+
200
+ raise ArgumentError, 'criterion must be an Array of size 3', caller(1..-1)
201
+ end
202
+
203
+ def validate_scope!(scope)
204
+ return if scope.is_a?(Cuprum::Collections::Scopes::Base)
205
+
206
+ raise ArgumentError, 'scope must be a Scope instance', caller(1..-1)
207
+ end
208
+
209
+ def validate_scopes!(scopes)
210
+ unless scopes.is_a?(Array)
211
+ raise ArgumentError, 'scopes must be an Array', caller(1..-1)
212
+ end
213
+
214
+ return if scopes.all? do |scope|
215
+ scope.is_a?(Cuprum::Collections::Scopes::Base)
216
+ end
217
+
218
+ raise ArgumentError, 'scope must be a Scope instance', caller(1..-1)
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/scopes'
4
+
5
+ module Cuprum::Collections::Scopes
6
+ # Defines a fluent interface for composing scopes.
7
+ module Composition
8
+ # @overload and(hash = nil, &block)
9
+ # Parses the hash or block and combines using a logical AND.
10
+ #
11
+ # @see Cuprum::Collections::Scopes::Criteria::Parser#parse.
12
+ #
13
+ # @overload and(scope)
14
+ # Combines with the current scope using a logical AND.
15
+ #
16
+ # Returns self if the given scope is empty.
17
+ def and(*args, &)
18
+ if scope?(args.first)
19
+ return and_scope(args.first) || and_generic_scope(args.first)
20
+ end
21
+
22
+ scope = builder.build(*args, &)
23
+
24
+ # We control the current and generated scopes, so we can skip validation
25
+ # and transformation.
26
+ builder.build_conjunction_scope(scopes: [self, scope], safe: false)
27
+ end
28
+ alias where and
29
+
30
+ # @overload not(hash = nil, &block)
31
+ # Parses and inverts the hash or block and combines using a logical AND.
32
+ #
33
+ # @see Cuprum::Collections::Scopes::Criteria::Parser#parse.
34
+ #
35
+ # @overload not(scope)
36
+ # Inverts and combines with the current scope using a logical AND.
37
+ #
38
+ # Returns self if the given scope is empty.
39
+ def not(...)
40
+ scope = builder.build(...).invert
41
+
42
+ self.and(scope)
43
+ end
44
+
45
+ # @overload and(hash = nil, &block)
46
+ # Parses the hash or block and combines using a logical OR.
47
+ #
48
+ # @see Cuprum::Collections::Scopes::Criteria::Parser#parse.
49
+ #
50
+ # @overload and(scope)
51
+ # Combines with the current scope using a logical OR.
52
+ #
53
+ # Returns self if the given scope is empty.
54
+ def or(*args, &)
55
+ if scope?(args.first)
56
+ return or_scope(args.first) || or_generic_scope(args.first)
57
+ end
58
+
59
+ scope = builder.build(*args, &)
60
+
61
+ # We control the current and generated scopes, so we can skip validation
62
+ # and transformation.
63
+ builder.build_disjunction_scope(scopes: [self, scope], safe: false)
64
+ end
65
+
66
+ private
67
+
68
+ def and_all_scope(_)
69
+ self
70
+ end
71
+
72
+ def and_criteria_scope(scope)
73
+ and_generic_scope(scope)
74
+ end
75
+
76
+ def and_conjunction_scope(scope)
77
+ scopes = scope.scopes.map do |inner|
78
+ builder.transform_scope(scope: inner)
79
+ end
80
+
81
+ builder.build_conjunction_scope(scopes: [self, *scopes], safe: false)
82
+ end
83
+
84
+ def and_disjunction_scope(scope)
85
+ and_generic_scope(scope)
86
+ end
87
+
88
+ def and_generic_scope(scope)
89
+ scope = builder.transform_scope(scope:)
90
+
91
+ # We control the current and generated scopes, so we can skip validation
92
+ # and transformation.
93
+ builder.build_conjunction_scope(scopes: [self, scope], safe: false)
94
+ end
95
+
96
+ def and_scope(scope) # rubocop:disable Metrics/MethodLength
97
+ return self if scope.empty?
98
+
99
+ case scope.type
100
+ when :all
101
+ and_all_scope(scope)
102
+ when :conjunction
103
+ and_conjunction_scope(scope)
104
+ when :criteria
105
+ and_criteria_scope(scope)
106
+ when :disjunction
107
+ and_disjunction_scope(scope)
108
+ when :none
109
+ scope
110
+ end
111
+ end
112
+
113
+ def or_all_scope(scope)
114
+ builder.transform_scope(scope:)
115
+ end
116
+
117
+ def or_conjunction_scope(scope)
118
+ or_generic_scope(scope)
119
+ end
120
+
121
+ def or_criteria_scope(scope)
122
+ or_generic_scope(scope)
123
+ end
124
+
125
+ def or_disjunction_scope(scope)
126
+ scopes = scope.scopes.map do |inner|
127
+ builder.transform_scope(scope: inner)
128
+ end
129
+
130
+ builder.build_disjunction_scope(scopes: [self, *scopes], safe: false)
131
+ end
132
+
133
+ def or_generic_scope(scope)
134
+ scope = builder.transform_scope(scope:)
135
+
136
+ # We control the current and generated scopes, so we can skip validation
137
+ # and transformation.
138
+ builder.build_disjunction_scope(scopes: [self, scope], safe: false)
139
+ end
140
+
141
+ def or_scope(scope) # rubocop:disable Metrics/MethodLength
142
+ return self if scope.empty?
143
+
144
+ case scope.type
145
+ when :all
146
+ or_all_scope(scope)
147
+ when :conjunction
148
+ or_conjunction_scope(scope)
149
+ when :criteria
150
+ or_criteria_scope(scope)
151
+ when :disjunction
152
+ or_disjunction_scope(scope)
153
+ when :none
154
+ self
155
+ end
156
+ end
157
+
158
+ def scope?(value)
159
+ value.is_a?(Cuprum::Collections::Scopes::Base)
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/scopes'
4
+ require 'cuprum/collections/scopes/container'
5
+
6
+ module Cuprum::Collections::Scopes
7
+ # Functionality for implementing a logical AND scope.
8
+ module Conjunction
9
+ include Cuprum::Collections::Scopes::Container
10
+
11
+ # (see Cuprum::Collections::Scopes::Composition#and)
12
+ def and(*args, &)
13
+ return super if scope?(args.first)
14
+
15
+ with_scopes([*scopes, builder.build(*args, &)])
16
+ end
17
+ alias where and
18
+
19
+ # @return [Cuprum::Collections::Disjunction] a logical OR scope with the
20
+ # constituent scopes inverted.
21
+ def invert
22
+ builder.build_disjunction_scope(scopes: scopes.map(&:invert))
23
+ end
24
+
25
+ # (see Cuprum::Collections::Scopes::Base#type)
26
+ def type
27
+ :conjunction
28
+ end
29
+
30
+ private
31
+
32
+ def and_conjunction_scope(scope)
33
+ scopes = scope.scopes.map do |inner|
34
+ builder.transform_scope(scope: inner)
35
+ end
36
+
37
+ with_scopes([*self.scopes, *scopes])
38
+ end
39
+
40
+ def and_generic_scope(scope)
41
+ with_scopes([*scopes, builder.transform_scope(scope:)])
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/scopes'
4
+ require 'cuprum/collections/scopes/base'
5
+ require 'cuprum/collections/scopes/conjunction'
6
+
7
+ module Cuprum::Collections::Scopes
8
+ # Generic scope class for defining collection-independent logical AND scopes.
9
+ class ConjunctionScope < Cuprum::Collections::Scopes::Base
10
+ include Cuprum::Collections::Scopes::Conjunction
11
+ end
12
+ end