cuprum-collections 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 (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,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/queries'
4
+ require 'cuprum/collections/rspec'
5
+
6
+ module Cuprum::Collections::RSpec
7
+ OPERATORS = Cuprum::Collections::Queries::Operators
8
+ private_constant :OPERATORS
9
+
10
+ # Shared contexts for specs that define querying behavior.
11
+ QUERYING_CONTEXTS = lambda do
12
+ let(:filter) { nil }
13
+ let(:strategy) { nil }
14
+ let(:limit) { nil }
15
+ let(:offset) { nil }
16
+ let(:order) { nil }
17
+
18
+ shared_context 'when the query has limit: value' do
19
+ let(:limit) { 3 }
20
+ let(:matching_data) { super()[0...limit] }
21
+ end
22
+
23
+ shared_context 'when the query has offset: value' do
24
+ let(:offset) { 2 }
25
+ let(:matching_data) { super()[offset..] || [] }
26
+ end
27
+
28
+ shared_context 'when the query has order: a simple ordering' do
29
+ let(:order) { :title }
30
+ let(:matching_data) { super().sort_by { |item| item['title'] } }
31
+ end
32
+
33
+ shared_context 'when the query has order: a complex ordering' do
34
+ let(:order) do
35
+ {
36
+ author: :asc,
37
+ title: :desc
38
+ }
39
+ end
40
+ let(:matching_data) do
41
+ super().sort do |u, v|
42
+ cmp = u['author'] <=> v['author']
43
+
44
+ cmp.zero? ? (v['title'] <=> u['title']) : cmp
45
+ end
46
+ end
47
+ end
48
+
49
+ shared_context 'when the query has where: a simple block filter' do
50
+ let(:filter) { -> { { author: 'Ursula K. LeGuin' } } }
51
+ let(:matching_data) do
52
+ super().select { |item| item['author'] == 'Ursula K. LeGuin' }
53
+ end
54
+ end
55
+
56
+ shared_context 'when the query has where: a complex block filter' do
57
+ let(:filter) do
58
+ lambda do
59
+ {
60
+ author: equals('Ursula K. LeGuin'),
61
+ series: not_equal('Earthsea')
62
+ }
63
+ end
64
+ end
65
+ let(:matching_data) do
66
+ super()
67
+ .select { |item| item['author'] == 'Ursula K. LeGuin' }
68
+ .reject { |item| item['series'] == 'Earthsea' }
69
+ end
70
+ end
71
+
72
+ shared_context 'when the query has where: a greater_than filter' do
73
+ let(:filter) { -> { { published_at: greater_than('1970-12-01') } } }
74
+ let(:matching_data) do
75
+ super().select { |item| item['published_at'] > '1970-12-01' }
76
+ end
77
+ end
78
+
79
+ shared_context 'when the query has where: a greater_than_or_equal_to' \
80
+ ' filter' \
81
+ do
82
+ let(:filter) do
83
+ -> { { published_at: greater_than_or_equal_to('1970-12-01') } }
84
+ end
85
+ let(:matching_data) do
86
+ super().select { |item| item['published_at'] >= '1970-12-01' }
87
+ end
88
+ end
89
+
90
+ shared_context 'when the query has where: a less_than filter' do
91
+ let(:filter) { -> { { published_at: less_than('1970-12-01') } } }
92
+ let(:matching_data) do
93
+ super().select { |item| item['published_at'] < '1970-12-01' }
94
+ end
95
+ end
96
+
97
+ shared_context 'when the query has where: a less_than_or_equal_to filter' do
98
+ let(:filter) do
99
+ -> { { published_at: less_than_or_equal_to('1970-12-01') } }
100
+ end
101
+ let(:matching_data) do
102
+ super().select { |item| item['published_at'] <= '1970-12-01' }
103
+ end
104
+ end
105
+
106
+ shared_context 'when the query has where: an equal block filter' do
107
+ let(:filter) { -> { { author: equals('Ursula K. LeGuin') } } }
108
+ let(:matching_data) do
109
+ super().select { |item| item['author'] == 'Ursula K. LeGuin' }
110
+ end
111
+ end
112
+
113
+ shared_context 'when the query has where: a not_equal block filter' do
114
+ let(:filter) { -> { { author: not_equal('Ursula K. LeGuin') } } }
115
+ let(:matching_data) do
116
+ super().reject { |item| item['author'] == 'Ursula K. LeGuin' }
117
+ end
118
+ end
119
+
120
+ shared_context 'when the query has where: a not_one_of block filter' do
121
+ let(:filter) do
122
+ -> { { series: not_one_of(['Earthsea', 'The Lord of the Rings']) } }
123
+ end
124
+ let(:matching_data) do
125
+ super().reject do |item|
126
+ ['Earthsea', 'The Lord of the Rings'].include?(item['series'])
127
+ end
128
+ end
129
+ end
130
+
131
+ shared_context 'when the query has where: a one_of block filter' do
132
+ let(:filter) do
133
+ -> { { series: one_of(['Earthsea', 'The Lord of the Rings']) } }
134
+ end
135
+ let(:matching_data) do
136
+ super().select do |item|
137
+ ['Earthsea', 'The Lord of the Rings'].include?(item['series'])
138
+ end
139
+ end
140
+ end
141
+
142
+ shared_context 'when the query has multiple query options' do
143
+ let(:filter) { -> { { author: 'Ursula K. LeGuin' } } }
144
+ let(:strategy) { nil }
145
+ let(:order) { { title: :desc } }
146
+ let(:limit) { 2 }
147
+ let(:offset) { 1 }
148
+ let(:matching_data) do
149
+ super()
150
+ .select { |item| item['author'] == 'Ursula K. LeGuin' }
151
+ .sort { |u, v| v['title'] <=> u['title'] }
152
+ .slice(1, 2) || []
153
+ end
154
+ end
155
+ end
156
+
157
+ # Contract validating the behavior objects that perform queries.
158
+ QUERYING_CONTRACT = lambda do |block:, operators: OPERATORS.values|
159
+ wrap_context 'when the query has limit: value' do
160
+ instance_exec(&block)
161
+ end
162
+
163
+ wrap_context 'when the query has offset: value' do
164
+ instance_exec(&block)
165
+ end
166
+
167
+ wrap_context 'when the query has order: a simple ordering' do
168
+ instance_exec(&block)
169
+ end
170
+
171
+ wrap_context 'when the query has order: a complex ordering' do
172
+ instance_exec(&block)
173
+ end
174
+
175
+ context 'when the query has where: a block filter' do
176
+ context 'with a simple filter' do
177
+ include_context 'when the query has where: a simple block filter'
178
+
179
+ instance_exec(&block)
180
+ end
181
+
182
+ context 'with a complex filter' do
183
+ include_context 'when the query has where: a complex block filter'
184
+
185
+ if operators.include?(OPERATORS::EQUAL) &&
186
+ operators.include?(OPERATORS::NOT_EQUAL)
187
+ instance_exec(&block)
188
+ else
189
+ # :nocov:
190
+ pending
191
+ # :nocov:
192
+ end
193
+ end
194
+
195
+ context 'with an equals filter' do
196
+ include_context 'when the query has where: an equal block filter'
197
+
198
+ if operators.include?(OPERATORS::EQUAL)
199
+ instance_exec(&block)
200
+ else
201
+ # :nocov:
202
+ pending
203
+ # :nocov:
204
+ end
205
+ end
206
+
207
+ context 'with a greater_than filter' do
208
+ include_context 'when the query has where: a greater_than filter'
209
+
210
+ if operators.include?(OPERATORS::GREATER_THAN)
211
+ instance_exec(&block)
212
+ else
213
+ # :nocov:
214
+ pending
215
+ # :nocov:
216
+ end
217
+ end
218
+
219
+ context 'with a greater_than_or_equal_to filter' do
220
+ include_context \
221
+ 'when the query has where: a greater_than_or_equal_to filter'
222
+
223
+ if operators.include?(OPERATORS::GREATER_THAN_OR_EQUAL_TO)
224
+ instance_exec(&block)
225
+ else
226
+ # :nocov:
227
+ pending
228
+ # :nocov:
229
+ end
230
+ end
231
+
232
+ context 'with a less_than filter' do
233
+ include_context 'when the query has where: a less_than filter'
234
+
235
+ if operators.include?(OPERATORS::LESS_THAN)
236
+ instance_exec(&block)
237
+ else
238
+ # :nocov:
239
+ pending
240
+ # :nocov:
241
+ end
242
+ end
243
+
244
+ context 'with a less_than_or_equal_to filter' do
245
+ include_context \
246
+ 'when the query has where: a less_than_or_equal_to filter'
247
+
248
+ if operators.include?(OPERATORS::LESS_THAN_OR_EQUAL_TO)
249
+ instance_exec(&block)
250
+ else
251
+ # :nocov:
252
+ pending
253
+ # :nocov:
254
+ end
255
+ end
256
+
257
+ context 'with a not_equal filter' do
258
+ include_context 'when the query has where: a not_equal block filter'
259
+
260
+ if operators.include?(OPERATORS::NOT_EQUAL)
261
+ instance_exec(&block)
262
+ else
263
+ # :nocov:
264
+ pending
265
+ # :nocov:
266
+ end
267
+ end
268
+
269
+ context 'with a not_one_of filter' do
270
+ include_context 'when the query has where: a not_one_of block filter'
271
+
272
+ if operators.include?(OPERATORS::NOT_ONE_OF)
273
+ instance_exec(&block)
274
+ else
275
+ # :nocov:
276
+ pending
277
+ # :nocov:
278
+ end
279
+ end
280
+
281
+ context 'with a one_of filter' do
282
+ include_context 'when the query has where: a one_of block filter'
283
+
284
+ if operators.include?(OPERATORS::ONE_OF)
285
+ instance_exec(&block)
286
+ else
287
+ # :nocov:
288
+ pending
289
+ # :nocov:
290
+ end
291
+ end
292
+ end
293
+
294
+ wrap_context 'when the query has multiple query options' do
295
+ instance_exec(&block)
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/rspec'
4
+
5
+ module Cuprum::Collections::RSpec
6
+ # Contract validating the behavior of an UpdateOne command implementation.
7
+ UPDATE_ONE_COMMAND_CONTRACT = lambda do
8
+ describe '#call' do
9
+ let(:mapped_data) do
10
+ defined?(super()) ? super() : data
11
+ end
12
+ let(:matching_data) { attributes }
13
+ let(:expected_data) do
14
+ defined?(super()) ? super() : matching_data
15
+ end
16
+ let(:primary_key_name) do
17
+ defined?(super()) ? super() : :id
18
+ end
19
+ let(:scoped) do
20
+ key = primary_key_name
21
+ value = entity[primary_key_name.to_s]
22
+
23
+ query.where { { key => value } }
24
+ end
25
+
26
+ it 'should validate the :entity keyword' do
27
+ expect(command)
28
+ .to validate_parameter(:call, :entity)
29
+ .using_constraint(entity_type)
30
+ end
31
+
32
+ context 'when the item does not exist in the collection' do
33
+ let(:expected_error) do
34
+ Cuprum::Collections::Errors::NotFound.new(
35
+ collection_name: collection_name,
36
+ primary_key_name: primary_key_name,
37
+ primary_key_values: attributes[primary_key_name]
38
+ )
39
+ end
40
+ let(:matching_data) { mapped_data.first }
41
+
42
+ it 'should return a failing result' do
43
+ expect(command.call(entity: entity))
44
+ .to be_a_failing_result
45
+ .with_error(expected_error)
46
+ end
47
+
48
+ it 'should not append an item to the collection' do
49
+ expect { command.call(entity: entity) }
50
+ .not_to(change { query.reset.count })
51
+ end
52
+ end
53
+
54
+ context 'when the item exists in the collection' do
55
+ let(:data) { fixtures_data }
56
+ let(:matching_data) do
57
+ mapped_data.first.merge(super())
58
+ end
59
+
60
+ it 'should return a passing result' do
61
+ expect(command.call(entity: entity))
62
+ .to be_a_passing_result
63
+ .with_value(be == expected_data)
64
+ end
65
+
66
+ it 'should not append an item to the collection' do
67
+ expect { command.call(entity: entity) }
68
+ .not_to(change { query.reset.count })
69
+ end
70
+
71
+ it 'should set the attributes' do
72
+ command.call(entity: entity)
73
+
74
+ expect(scoped.to_a.first).to be == expected_data
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/rspec'
4
+
5
+ module Cuprum::Collections::RSpec
6
+ # Contract validating the behavior of a ValidateOne command implementation.
7
+ VALIDATE_ONE_COMMAND_CONTRACT = lambda do |default_contract:|
8
+ describe '#call' do
9
+ it 'should validate the :contract keyword' do
10
+ expect(command)
11
+ .to validate_parameter(:call, :contract)
12
+ .with_value(Object.new.freeze)
13
+ .using_constraint(Stannum::Constraints::Base, optional: true)
14
+ end
15
+
16
+ it 'should validate the :entity keyword' do
17
+ expect(command)
18
+ .to validate_parameter(:call, :entity)
19
+ .with_value(Object.new.freeze)
20
+ .using_constraint(entity_type)
21
+ end
22
+
23
+ describe 'with contract: nil' do
24
+ if default_contract
25
+ context 'when the entity does not match the default contract' do
26
+ let(:attributes) { invalid_default_attributes }
27
+ let(:expected_error) do
28
+ Cuprum::Collections::Errors::FailedValidation.new(
29
+ entity_class: entity.class,
30
+ errors: expected_errors
31
+ )
32
+ end
33
+
34
+ it 'should return a failing result' do
35
+ expect(command.call(entity: entity))
36
+ .to be_a_failing_result
37
+ .with_error(expected_error)
38
+ end
39
+ end
40
+
41
+ context 'when the entity matches the default contract' do
42
+ let(:attributes) { valid_default_attributes }
43
+
44
+ it 'should return a passing result' do
45
+ expect(command.call(entity: entity))
46
+ .to be_a_passing_result
47
+ .with_value(entity)
48
+ end
49
+ end
50
+ else
51
+ let(:attributes) { valid_attributes }
52
+ let(:expected_error) do
53
+ Cuprum::Collections::Errors::MissingDefaultContract.new(
54
+ entity_class: entity.class
55
+ )
56
+ end
57
+
58
+ it 'should return a failing result' do
59
+ expect(command.call(entity: entity))
60
+ .to be_a_failing_result
61
+ .with_error(expected_error)
62
+ end
63
+ end
64
+ end
65
+
66
+ describe 'with contract: value' do
67
+ context 'when the entity does not match the contract' do
68
+ let(:attributes) { invalid_attributes }
69
+ let(:errors) { contract.errors_for(entity) }
70
+ let(:expected_error) do
71
+ Cuprum::Collections::Errors::FailedValidation.new(
72
+ entity_class: entity.class,
73
+ errors: errors
74
+ )
75
+ end
76
+
77
+ it 'should return a failing result' do
78
+ expect(command.call(contract: contract, entity: entity))
79
+ .to be_a_failing_result
80
+ .with_error(expected_error)
81
+ end
82
+ end
83
+
84
+ context 'when the entity matches the contract' do
85
+ let(:attributes) { valid_attributes }
86
+
87
+ it 'should return a passing result' do
88
+ expect(command.call(contract: contract, entity: entity))
89
+ .to be_a_passing_result
90
+ .with_value(entity)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections'
4
+
5
+ module Cuprum::Collections
6
+ # Namespace for RSpec contracts, which validate collection implementations.
7
+ module RSpec; end
8
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cuprum
4
+ module Collections
5
+ # @api private
6
+ #
7
+ # The current version of the gem.
8
+ #
9
+ # @see http://semver.org/
10
+ module Version
11
+ # Major version.
12
+ MAJOR = 0
13
+ # Minor version.
14
+ MINOR = 1
15
+ # Patch version.
16
+ PATCH = 0
17
+ # Prerelease version.
18
+ PRERELEASE = nil
19
+ # Build metadata.
20
+ BUILD = nil
21
+
22
+ class << self
23
+ # Generates the gem version string from the Version constants.
24
+ #
25
+ # Inlined here because dependencies may not be loaded when processing a
26
+ # gemspec, which results in the user being unable to install the gem for
27
+ # the first time.
28
+ #
29
+ # @see SleepingKingStudios::Tools::SemanticVersion#to_gem_version
30
+ def to_gem_version
31
+ str = +"#{MAJOR}.#{MINOR}.#{PATCH}"
32
+
33
+ prerelease = value_of(:PRERELEASE)
34
+ str << ".#{prerelease}" if prerelease
35
+
36
+ build = value_of(:BUILD)
37
+ str << ".#{build}" if build
38
+
39
+ str
40
+ end
41
+
42
+ private
43
+
44
+ def value_of(constant)
45
+ return nil unless const_defined?(constant)
46
+
47
+ value = const_get(constant)
48
+
49
+ return nil if value.respond_to?(:empty?) && value.empty?
50
+
51
+ value
52
+ end
53
+ end
54
+ end
55
+
56
+ # @return [String] the current version of the gem.
57
+ VERSION = Version.to_gem_version
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum'
4
+
5
+ # A Ruby implementation of the command pattern.
6
+ module Cuprum
7
+ # A data abstraction layer based on the Cuprum library.
8
+ module Collections
9
+ autoload :Base, 'cuprum/collections/base'
10
+ autoload :Basic, 'cuprum/collections/basic'
11
+ autoload :Command, 'cuprum/collections/command'
12
+
13
+ # @return [String] the absolute path to the gem directory.
14
+ def self.gem_path
15
+ sep = File::SEPARATOR
16
+ pattern = /#{sep}lib#{sep}cuprum#{sep}?\z/
17
+
18
+ __dir__.sub(pattern, '')
19
+ end
20
+
21
+ # @return [String] The current version of the gem.
22
+ def self.version
23
+ VERSION
24
+ end
25
+ end
26
+ end