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,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