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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +321 -15
- data/lib/cuprum/collections/basic/collection.rb +13 -0
- data/lib/cuprum/collections/basic/commands/destroy_one.rb +4 -3
- data/lib/cuprum/collections/basic/commands/find_many.rb +1 -1
- data/lib/cuprum/collections/basic/commands/insert_one.rb +4 -3
- data/lib/cuprum/collections/basic/commands/update_one.rb +4 -3
- data/lib/cuprum/collections/basic/query.rb +3 -3
- data/lib/cuprum/collections/basic/repository.rb +67 -0
- data/lib/cuprum/collections/commands/abstract_find_many.rb +33 -32
- data/lib/cuprum/collections/commands/abstract_find_one.rb +4 -3
- data/lib/cuprum/collections/commands/create.rb +60 -0
- data/lib/cuprum/collections/commands/find_one_matching.rb +134 -0
- data/lib/cuprum/collections/commands/update.rb +74 -0
- data/lib/cuprum/collections/commands/upsert.rb +162 -0
- data/lib/cuprum/collections/commands.rb +7 -2
- data/lib/cuprum/collections/errors/abstract_find_error.rb +210 -0
- data/lib/cuprum/collections/errors/already_exists.rb +4 -72
- data/lib/cuprum/collections/errors/extra_attributes.rb +8 -18
- data/lib/cuprum/collections/errors/failed_validation.rb +5 -18
- data/lib/cuprum/collections/errors/invalid_parameters.rb +7 -15
- data/lib/cuprum/collections/errors/invalid_query.rb +5 -15
- data/lib/cuprum/collections/errors/missing_default_contract.rb +5 -17
- data/lib/cuprum/collections/errors/not_found.rb +4 -67
- data/lib/cuprum/collections/errors/not_unique.rb +18 -0
- data/lib/cuprum/collections/errors/unknown_operator.rb +7 -17
- data/lib/cuprum/collections/errors.rb +13 -1
- data/lib/cuprum/collections/queries/ordering.rb +4 -2
- data/lib/cuprum/collections/repository.rb +105 -0
- data/lib/cuprum/collections/rspec/assign_one_command_contract.rb +2 -2
- data/lib/cuprum/collections/rspec/build_one_command_contract.rb +1 -1
- data/lib/cuprum/collections/rspec/collection_contract.rb +140 -103
- data/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +8 -6
- data/lib/cuprum/collections/rspec/find_many_command_contract.rb +114 -34
- data/lib/cuprum/collections/rspec/find_one_command_contract.rb +12 -9
- data/lib/cuprum/collections/rspec/insert_one_command_contract.rb +4 -3
- data/lib/cuprum/collections/rspec/query_contract.rb +3 -3
- data/lib/cuprum/collections/rspec/querying_contract.rb +2 -2
- data/lib/cuprum/collections/rspec/repository_contract.rb +235 -0
- data/lib/cuprum/collections/rspec/update_one_command_contract.rb +4 -3
- data/lib/cuprum/collections/version.rb +3 -3
- data/lib/cuprum/collections.rb +1 -0
- 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: '
|
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: '
|
127
|
+
author: 'Tamsyn Muir',
|
128
128
|
series: 'The Locked Tomb',
|
129
129
|
category: 'Horror'
|
130
130
|
}
|
@@ -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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
30
|
+
it { expect(collection).to define_constant(class_name) }
|
19
31
|
|
20
|
-
|
32
|
+
it { expect(collection.const_get(class_name)).to be_a Class }
|
21
33
|
|
22
|
-
|
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))
|
42
|
-
be ==
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
43
|
+
describe 'with options' do
|
44
|
+
let(:constructor_options) do
|
45
|
+
{
|
46
|
+
data: [],
|
47
|
+
member_name: 'tome'
|
48
|
+
}
|
49
|
+
end
|
56
50
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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))
|
84
|
-
be ==
|
85
|
-
|
86
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
114
|
+
include_examples 'should define the command',
|
115
|
+
:destroy_one,
|
116
|
+
commands_namespace::DestroyOne
|
101
117
|
|
102
|
-
|
103
|
-
|
104
|
-
|
118
|
+
include_examples 'should define the command',
|
119
|
+
:find_many,
|
120
|
+
commands_namespace::FindMany
|
105
121
|
|
106
|
-
|
107
|
-
|
108
|
-
|
122
|
+
include_examples 'should define the command',
|
123
|
+
:find_matching,
|
124
|
+
commands_namespace::FindMatching
|
109
125
|
|
110
|
-
|
111
|
-
|
112
|
-
|
126
|
+
include_examples 'should define the command',
|
127
|
+
:find_one,
|
128
|
+
commands_namespace::FindOne
|
113
129
|
|
114
|
-
|
115
|
-
|
116
|
-
|
130
|
+
include_examples 'should define the command',
|
131
|
+
:insert_one,
|
132
|
+
commands_namespace::InsertOne
|
117
133
|
|
118
|
-
|
119
|
-
|
120
|
-
|
134
|
+
include_examples 'should define the command',
|
135
|
+
:update_one,
|
136
|
+
commands_namespace::UpdateOne
|
121
137
|
|
122
|
-
|
123
|
-
|
124
|
-
|
138
|
+
include_examples 'should define the command',
|
139
|
+
:validate_one,
|
140
|
+
commands_namespace::ValidateOne
|
125
141
|
|
126
|
-
|
127
|
-
|
128
|
-
|
142
|
+
describe '#collection_name' do
|
143
|
+
include_examples 'should define reader',
|
144
|
+
:collection_name,
|
145
|
+
-> { an_instance_of(String) }
|
146
|
+
end
|
129
147
|
|
130
|
-
|
131
|
-
|
132
|
-
let(:query) { collection.query }
|
148
|
+
describe '#count' do
|
149
|
+
it { expect(collection).to respond_to(:count).with(0).arguments }
|
133
150
|
|
134
|
-
|
151
|
+
it { expect(collection).to have_aliased_method(:count).as(:size) }
|
135
152
|
|
136
|
-
|
153
|
+
it { expect(collection.count).to be 0 }
|
137
154
|
|
138
|
-
|
139
|
-
|
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
|
-
|
160
|
+
describe '#qualified_name' do
|
161
|
+
include_examples 'should define reader',
|
162
|
+
:qualified_name,
|
163
|
+
-> { an_instance_of(String) }
|
164
|
+
end
|
145
165
|
|
146
|
-
|
166
|
+
describe '#query' do
|
167
|
+
let(:default_order) { defined?(super()) ? super() : {} }
|
168
|
+
let(:query) { collection.query }
|
147
169
|
|
148
|
-
|
170
|
+
it { expect(collection).to respond_to(:query).with(0).arguments }
|
149
171
|
|
150
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
|