cuprum-collections 0.1.0 → 0.3.0.rc.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +321 -15
  4. data/lib/cuprum/collections/basic/collection.rb +13 -0
  5. data/lib/cuprum/collections/basic/commands/destroy_one.rb +4 -3
  6. data/lib/cuprum/collections/basic/commands/find_many.rb +1 -1
  7. data/lib/cuprum/collections/basic/commands/insert_one.rb +4 -3
  8. data/lib/cuprum/collections/basic/commands/update_one.rb +4 -3
  9. data/lib/cuprum/collections/basic/query.rb +3 -3
  10. data/lib/cuprum/collections/basic/repository.rb +67 -0
  11. data/lib/cuprum/collections/commands/abstract_find_many.rb +33 -32
  12. data/lib/cuprum/collections/commands/abstract_find_one.rb +4 -3
  13. data/lib/cuprum/collections/commands/create.rb +60 -0
  14. data/lib/cuprum/collections/commands/find_one_matching.rb +134 -0
  15. data/lib/cuprum/collections/commands/update.rb +74 -0
  16. data/lib/cuprum/collections/commands/upsert.rb +162 -0
  17. data/lib/cuprum/collections/commands.rb +7 -2
  18. data/lib/cuprum/collections/errors/abstract_find_error.rb +210 -0
  19. data/lib/cuprum/collections/errors/already_exists.rb +4 -72
  20. data/lib/cuprum/collections/errors/extra_attributes.rb +8 -18
  21. data/lib/cuprum/collections/errors/failed_validation.rb +5 -18
  22. data/lib/cuprum/collections/errors/invalid_parameters.rb +7 -15
  23. data/lib/cuprum/collections/errors/invalid_query.rb +5 -15
  24. data/lib/cuprum/collections/errors/missing_default_contract.rb +5 -17
  25. data/lib/cuprum/collections/errors/not_found.rb +4 -67
  26. data/lib/cuprum/collections/errors/not_unique.rb +18 -0
  27. data/lib/cuprum/collections/errors/unknown_operator.rb +7 -17
  28. data/lib/cuprum/collections/errors.rb +13 -1
  29. data/lib/cuprum/collections/queries/ordering.rb +4 -2
  30. data/lib/cuprum/collections/repository.rb +105 -0
  31. data/lib/cuprum/collections/rspec/assign_one_command_contract.rb +2 -2
  32. data/lib/cuprum/collections/rspec/build_one_command_contract.rb +1 -1
  33. data/lib/cuprum/collections/rspec/collection_contract.rb +140 -103
  34. data/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +8 -6
  35. data/lib/cuprum/collections/rspec/find_many_command_contract.rb +114 -34
  36. data/lib/cuprum/collections/rspec/find_one_command_contract.rb +12 -9
  37. data/lib/cuprum/collections/rspec/insert_one_command_contract.rb +4 -3
  38. data/lib/cuprum/collections/rspec/query_contract.rb +3 -3
  39. data/lib/cuprum/collections/rspec/querying_contract.rb +2 -2
  40. data/lib/cuprum/collections/rspec/repository_contract.rb +235 -0
  41. data/lib/cuprum/collections/rspec/update_one_command_contract.rb +4 -3
  42. data/lib/cuprum/collections/version.rb +3 -3
  43. data/lib/cuprum/collections.rb +1 -0
  44. metadata +25 -91
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require 'cuprum/collections'
6
+
7
+ module Cuprum::Collections
8
+ # A repository represents a group of collections.
9
+ #
10
+ # Conceptually, a repository represents one or more underlying data stores. An
11
+ # application might have one repository for each data store, e.g. one
12
+ # repository for relational data, a second repository for document-based data,
13
+ # and so on. The application may instead aggregate all of its collections into
14
+ # a single repository, relying on the shared interface of all Collection
15
+ # implementations.
16
+ class Repository
17
+ extend Forwardable
18
+
19
+ # Error raised when trying to add an existing collection to the repository.
20
+ class DuplicateCollectionError < StandardError; end
21
+
22
+ # Error raised when trying to add an invalid collection to the repository.
23
+ class InvalidCollectionError < StandardError; end
24
+
25
+ # Error raised when trying to access a collection that is not defined.
26
+ class UndefinedCollectionError < StandardError; end
27
+
28
+ def initialize
29
+ @collections = {}
30
+ end
31
+
32
+ # @!method keys
33
+ # Returns the names of the collections in the repository.
34
+ #
35
+ # @return [Array<String>] the collection names.
36
+
37
+ def_delegators :@collections, :keys
38
+
39
+ # Finds and returns the collection with the given name.
40
+ #
41
+ # @param qualified_name [String, Symbol] The qualified name of the
42
+ # collection to return.
43
+ #
44
+ # @return [Object] the requested collection.
45
+ #
46
+ # @raise [Cuprum::Collection::Repository::UndefinedCOllectionError] if the
47
+ # requested collection is not in the repository.
48
+ def [](qualified_name)
49
+ @collections.fetch(qualified_name.to_s) do
50
+ raise UndefinedCollectionError,
51
+ "repository does not define collection #{qualified_name.inspect}"
52
+ end
53
+ end
54
+
55
+ # Adds the collection to the repository.
56
+ #
57
+ # The collection must implement the #collection_name property. Repository
58
+ # subclasses may enforce additional requirements.
59
+ #
60
+ # @param collection [#collection_name] The collection to add to the
61
+ # repository.
62
+ # @param force [true, false] If true, override an existing collection with
63
+ # the same name.
64
+ #
65
+ # @return [Cuprum::Rails::Repository] the repository.
66
+ #
67
+ # @raise [DuplicateCollectionError] if a collection with the same name
68
+ # already exists in the repository.
69
+ def add(collection, force: false)
70
+ validate_collection!(collection)
71
+
72
+ if !force && key?(collection.qualified_name.to_s)
73
+ raise DuplicateCollectionError,
74
+ "collection #{collection.qualified_name} already exists"
75
+ end
76
+
77
+ @collections[collection.qualified_name.to_s] = collection
78
+
79
+ self
80
+ end
81
+ alias << add
82
+
83
+ # Checks if a collection with the given name exists in the repository.
84
+ #
85
+ # @param qualified_name [String, Symbol] The name to check for.
86
+ #
87
+ # @return [true, false] true if the key exists, otherwise false.
88
+ def key?(qualified_name)
89
+ @collections.key?(qualified_name.to_s)
90
+ end
91
+
92
+ private
93
+
94
+ def valid_collection?(collection)
95
+ collection.respond_to?(:collection_name)
96
+ end
97
+
98
+ def validate_collection!(collection)
99
+ return if valid_collection?(collection)
100
+
101
+ raise InvalidCollectionError,
102
+ "#{collection.inspect} is not a valid collection"
103
+ end
104
+ end
105
+ end
@@ -58,7 +58,7 @@ module Cuprum::Collections::RSpec
58
58
  let(:attributes) do
59
59
  {
60
60
  title: 'Gideon the Ninth',
61
- author: 'Tammsyn Muir',
61
+ author: 'Tamsyn Muir',
62
62
  series: 'The Locked Tomb',
63
63
  category: 'Horror'
64
64
  }
@@ -124,7 +124,7 @@ module Cuprum::Collections::RSpec
124
124
  let(:attributes) do
125
125
  {
126
126
  title: 'Gideon the Ninth',
127
- author: 'Tammsyn Muir',
127
+ author: 'Tamsyn Muir',
128
128
  series: 'The Locked Tomb',
129
129
  category: 'Horror'
130
130
  }
@@ -50,7 +50,7 @@ module Cuprum::Collections::RSpec
50
50
  let(:attributes) do
51
51
  {
52
52
  title: 'Gideon the Ninth',
53
- author: 'Tammsyn Muir',
53
+ author: 'Tamsyn Muir',
54
54
  series: 'The Locked Tomb',
55
55
  category: 'Horror'
56
56
  }
@@ -1,153 +1,190 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rspec/sleeping_king_studios/contract'
4
+
3
5
  require 'cuprum/collections/rspec'
4
6
 
5
7
  module Cuprum::Collections::RSpec
6
8
  # Contract validating the behavior of a Collection.
7
- COLLECTION_CONTRACT = lambda do
8
- shared_examples 'should define the command' do |command_name, command_class|
9
- tools = SleepingKingStudios::Tools::Toolbelt.instance
10
- class_name = tools.str.camelize(command_name)
11
-
12
- describe "::#{class_name}" do
13
- let(:constructor_options) { {} }
14
- let(:command) do
15
- collection.const_get(class_name).new(**constructor_options)
16
- end
9
+ module CollectionContract
10
+ extend RSpec::SleepingKingStudios::Contract
11
+
12
+ # @!method apply(example_group)
13
+ # Adds the contract to the example group.
14
+ #
15
+ # @param example_group [RSpec::Core::ExampleGroup] The example group to
16
+ # which the contract is applied.
17
+
18
+ contract do
19
+ shared_examples 'should define the command' \
20
+ do |command_name, command_class|
21
+ tools = SleepingKingStudios::Tools::Toolbelt.instance
22
+ class_name = tools.str.camelize(command_name)
23
+
24
+ describe "::#{class_name}" do
25
+ let(:constructor_options) { {} }
26
+ let(:command) do
27
+ collection.const_get(class_name).new(**constructor_options)
28
+ end
17
29
 
18
- it { expect(collection).to define_constant(class_name) }
30
+ it { expect(collection).to define_constant(class_name) }
19
31
 
20
- it { expect(collection.const_get(class_name)).to be_a Class }
32
+ it { expect(collection.const_get(class_name)).to be_a Class }
21
33
 
22
- it { expect(collection.const_get(class_name)).to be < command_class }
23
-
24
- command_options.each do |option_name|
25
- it "should set the ##{option_name}" do
26
- expect(command.send(option_name))
27
- .to be == collection.send(option_name)
28
- end
29
- end
30
-
31
- describe 'with options' do
32
- let(:constructor_options) do
33
- {
34
- data: [],
35
- member_name: 'tome'
36
- }
37
- end
34
+ it { expect(collection.const_get(class_name)).to be < command_class }
38
35
 
39
36
  command_options.each do |option_name|
40
37
  it "should set the ##{option_name}" do
41
- expect(command.send(option_name)).to(
42
- be == constructor_options.fetch(option_name) do
43
- collection.send(option_name)
44
- end
45
- )
38
+ expect(command.send(option_name))
39
+ .to be == collection.send(option_name)
46
40
  end
47
41
  end
48
- end
49
- end
50
42
 
51
- describe "##{command_name}" do
52
- let(:constructor_options) { {} }
53
- let(:command) do
54
- collection.send(command_name, **constructor_options)
55
- end
43
+ describe 'with options' do
44
+ let(:constructor_options) do
45
+ {
46
+ data: [],
47
+ member_name: 'tome'
48
+ }
49
+ end
56
50
 
57
- it 'should define the command' do
58
- expect(collection)
59
- .to respond_to(command_name)
60
- .with(0).arguments
61
- .and_any_keywords
51
+ command_options.each do |option_name|
52
+ it "should set the ##{option_name}" do
53
+ expect(command.send(option_name)).to(
54
+ be == constructor_options.fetch(option_name) do
55
+ collection.send(option_name)
56
+ end
57
+ )
58
+ end
59
+ end
60
+ end
62
61
  end
63
62
 
64
- it { expect(command).to be_a collection.const_get(class_name) }
65
-
66
- command_options.each do |option_name|
67
- it "should set the ##{option_name}" do
68
- expect(command.send(option_name))
69
- .to be == collection.send(option_name)
63
+ describe "##{command_name}" do
64
+ let(:constructor_options) { {} }
65
+ let(:command) do
66
+ collection.send(command_name, **constructor_options)
70
67
  end
71
- end
72
68
 
73
- describe 'with options' do
74
- let(:constructor_options) do
75
- {
76
- data: [],
77
- member_name: 'tome'
78
- }
69
+ it 'should define the command' do
70
+ expect(collection)
71
+ .to respond_to(command_name)
72
+ .with(0).arguments
73
+ .and_any_keywords
79
74
  end
80
75
 
76
+ it { expect(command).to be_a collection.const_get(class_name) }
77
+
81
78
  command_options.each do |option_name|
82
79
  it "should set the ##{option_name}" do
83
- expect(command.send(option_name)).to(
84
- be == constructor_options.fetch(option_name) do
85
- collection.send(option_name)
86
- end
87
- )
80
+ expect(command.send(option_name))
81
+ .to be == collection.send(option_name)
82
+ end
83
+ end
84
+
85
+ describe 'with options' do
86
+ let(:constructor_options) do
87
+ {
88
+ data: [],
89
+ member_name: 'tome'
90
+ }
91
+ end
92
+
93
+ command_options.each do |option_name|
94
+ it "should set the ##{option_name}" do
95
+ expect(command.send(option_name)).to(
96
+ be == constructor_options.fetch(option_name) do
97
+ collection.send(option_name)
98
+ end
99
+ )
100
+ end
88
101
  end
89
102
  end
90
103
  end
91
104
  end
92
- end
93
105
 
94
- include_examples 'should define the command',
95
- :assign_one,
96
- commands_namespace::AssignOne
106
+ include_examples 'should define the command',
107
+ :assign_one,
108
+ commands_namespace::AssignOne
109
+
110
+ include_examples 'should define the command',
111
+ :build_one,
112
+ commands_namespace::BuildOne
97
113
 
98
- include_examples 'should define the command',
99
- :build_one,
100
- commands_namespace::BuildOne
114
+ include_examples 'should define the command',
115
+ :destroy_one,
116
+ commands_namespace::DestroyOne
101
117
 
102
- include_examples 'should define the command',
103
- :destroy_one,
104
- commands_namespace::DestroyOne
118
+ include_examples 'should define the command',
119
+ :find_many,
120
+ commands_namespace::FindMany
105
121
 
106
- include_examples 'should define the command',
107
- :find_many,
108
- commands_namespace::FindMany
122
+ include_examples 'should define the command',
123
+ :find_matching,
124
+ commands_namespace::FindMatching
109
125
 
110
- include_examples 'should define the command',
111
- :find_matching,
112
- commands_namespace::FindMatching
126
+ include_examples 'should define the command',
127
+ :find_one,
128
+ commands_namespace::FindOne
113
129
 
114
- include_examples 'should define the command',
115
- :find_one,
116
- commands_namespace::FindOne
130
+ include_examples 'should define the command',
131
+ :insert_one,
132
+ commands_namespace::InsertOne
117
133
 
118
- include_examples 'should define the command',
119
- :insert_one,
120
- commands_namespace::InsertOne
134
+ include_examples 'should define the command',
135
+ :update_one,
136
+ commands_namespace::UpdateOne
121
137
 
122
- include_examples 'should define the command',
123
- :update_one,
124
- commands_namespace::UpdateOne
138
+ include_examples 'should define the command',
139
+ :validate_one,
140
+ commands_namespace::ValidateOne
125
141
 
126
- include_examples 'should define the command',
127
- :validate_one,
128
- commands_namespace::ValidateOne
142
+ describe '#collection_name' do
143
+ include_examples 'should define reader',
144
+ :collection_name,
145
+ -> { an_instance_of(String) }
146
+ end
129
147
 
130
- describe '#query' do
131
- let(:default_order) { defined?(super()) ? super() : {} }
132
- let(:query) { collection.query }
148
+ describe '#count' do
149
+ it { expect(collection).to respond_to(:count).with(0).arguments }
133
150
 
134
- it { expect(collection).to respond_to(:query).with(0).arguments }
151
+ it { expect(collection).to have_aliased_method(:count).as(:size) }
135
152
 
136
- it { expect(collection.query).to be_a query_class }
153
+ it { expect(collection.count).to be 0 }
137
154
 
138
- it 'should set the query options' do
139
- query_options.each do |option, value|
140
- expect(collection.query.send option).to be == value
155
+ wrap_context 'when the collection has many items' do
156
+ it { expect(collection.count).to be items.count }
141
157
  end
142
158
  end
143
159
 
144
- it { expect(query.criteria).to be == [] }
160
+ describe '#qualified_name' do
161
+ include_examples 'should define reader',
162
+ :qualified_name,
163
+ -> { an_instance_of(String) }
164
+ end
145
165
 
146
- it { expect(query.limit).to be nil }
166
+ describe '#query' do
167
+ let(:default_order) { defined?(super()) ? super() : {} }
168
+ let(:query) { collection.query }
147
169
 
148
- it { expect(query.offset).to be nil }
170
+ it { expect(collection).to respond_to(:query).with(0).arguments }
149
171
 
150
- it { expect(query.order).to be == default_order }
172
+ it { expect(collection.query).to be_a query_class }
173
+
174
+ it 'should set the query options' do
175
+ query_options.each do |option, value|
176
+ expect(collection.query.send option).to be == value
177
+ end
178
+ end
179
+
180
+ it { expect(query.criteria).to be == [] }
181
+
182
+ it { expect(query.limit).to be nil }
183
+
184
+ it { expect(query.offset).to be nil }
185
+
186
+ it { expect(query.order).to be == default_order }
187
+ end
151
188
  end
152
189
  end
153
190
  end
@@ -28,9 +28,10 @@ module Cuprum::Collections::RSpec
28
28
  let(:primary_key) { invalid_primary_key_value }
29
29
  let(:expected_error) do
30
30
  Cuprum::Collections::Errors::NotFound.new(
31
- collection_name: command.collection_name,
32
- primary_key_name: primary_key_name,
33
- primary_key_values: primary_key
31
+ attribute_name: primary_key_name,
32
+ attribute_value: primary_key,
33
+ collection_name: collection_name,
34
+ primary_key: true
34
35
  )
35
36
  end
36
37
 
@@ -59,9 +60,10 @@ module Cuprum::Collections::RSpec
59
60
  let(:primary_key) { invalid_primary_key_value }
60
61
  let(:expected_error) do
61
62
  Cuprum::Collections::Errors::NotFound.new(
62
- collection_name: command.collection_name,
63
- primary_key_name: primary_key_name,
64
- primary_key_values: primary_key
63
+ attribute_name: primary_key_name,
64
+ attribute_value: primary_key,
65
+ collection_name: collection_name,
66
+ primary_key: true
65
67
  )
66
68
  end
67
69