cuprum-collections 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +59 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/DEVELOPMENT.md +25 -0
- data/LICENSE +22 -0
- data/README.md +950 -0
- data/lib/cuprum/collections/base.rb +11 -0
- data/lib/cuprum/collections/basic/collection.rb +135 -0
- data/lib/cuprum/collections/basic/command.rb +112 -0
- data/lib/cuprum/collections/basic/commands/assign_one.rb +54 -0
- data/lib/cuprum/collections/basic/commands/build_one.rb +45 -0
- data/lib/cuprum/collections/basic/commands/destroy_one.rb +48 -0
- data/lib/cuprum/collections/basic/commands/find_many.rb +65 -0
- data/lib/cuprum/collections/basic/commands/find_matching.rb +126 -0
- data/lib/cuprum/collections/basic/commands/find_one.rb +49 -0
- data/lib/cuprum/collections/basic/commands/insert_one.rb +50 -0
- data/lib/cuprum/collections/basic/commands/update_one.rb +52 -0
- data/lib/cuprum/collections/basic/commands/validate_one.rb +69 -0
- data/lib/cuprum/collections/basic/commands.rb +18 -0
- data/lib/cuprum/collections/basic/query.rb +160 -0
- data/lib/cuprum/collections/basic/query_builder.rb +69 -0
- data/lib/cuprum/collections/basic/rspec/command_contract.rb +392 -0
- data/lib/cuprum/collections/basic/rspec.rb +8 -0
- data/lib/cuprum/collections/basic.rb +22 -0
- data/lib/cuprum/collections/command.rb +26 -0
- data/lib/cuprum/collections/commands/abstract_find_many.rb +77 -0
- data/lib/cuprum/collections/commands/abstract_find_matching.rb +64 -0
- data/lib/cuprum/collections/commands/abstract_find_one.rb +44 -0
- data/lib/cuprum/collections/commands.rb +8 -0
- data/lib/cuprum/collections/constraints/attribute_name.rb +22 -0
- data/lib/cuprum/collections/constraints/order/attributes_array.rb +26 -0
- data/lib/cuprum/collections/constraints/order/attributes_hash.rb +27 -0
- data/lib/cuprum/collections/constraints/order/complex_ordering.rb +46 -0
- data/lib/cuprum/collections/constraints/order/sort_direction.rb +32 -0
- data/lib/cuprum/collections/constraints/order.rb +8 -0
- data/lib/cuprum/collections/constraints/ordering.rb +114 -0
- data/lib/cuprum/collections/constraints/query_hash.rb +25 -0
- data/lib/cuprum/collections/constraints.rb +8 -0
- data/lib/cuprum/collections/errors/already_exists.rb +86 -0
- data/lib/cuprum/collections/errors/extra_attributes.rb +66 -0
- data/lib/cuprum/collections/errors/failed_validation.rb +66 -0
- data/lib/cuprum/collections/errors/invalid_parameters.rb +50 -0
- data/lib/cuprum/collections/errors/invalid_query.rb +55 -0
- data/lib/cuprum/collections/errors/missing_default_contract.rb +49 -0
- data/lib/cuprum/collections/errors/not_found.rb +81 -0
- data/lib/cuprum/collections/errors/unknown_operator.rb +71 -0
- data/lib/cuprum/collections/errors.rb +8 -0
- data/lib/cuprum/collections/queries/ordering.rb +74 -0
- data/lib/cuprum/collections/queries/parse.rb +22 -0
- data/lib/cuprum/collections/queries/parse_block.rb +206 -0
- data/lib/cuprum/collections/queries/parse_strategy.rb +91 -0
- data/lib/cuprum/collections/queries.rb +25 -0
- data/lib/cuprum/collections/query.rb +247 -0
- data/lib/cuprum/collections/query_builder.rb +61 -0
- data/lib/cuprum/collections/rspec/assign_one_command_contract.rb +168 -0
- data/lib/cuprum/collections/rspec/build_one_command_contract.rb +93 -0
- data/lib/cuprum/collections/rspec/collection_contract.rb +153 -0
- data/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +106 -0
- data/lib/cuprum/collections/rspec/find_many_command_contract.rb +327 -0
- data/lib/cuprum/collections/rspec/find_matching_command_contract.rb +194 -0
- data/lib/cuprum/collections/rspec/find_one_command_contract.rb +154 -0
- data/lib/cuprum/collections/rspec/fixtures.rb +89 -0
- data/lib/cuprum/collections/rspec/insert_one_command_contract.rb +83 -0
- data/lib/cuprum/collections/rspec/query_builder_contract.rb +92 -0
- data/lib/cuprum/collections/rspec/query_contract.rb +650 -0
- data/lib/cuprum/collections/rspec/querying_contract.rb +298 -0
- data/lib/cuprum/collections/rspec/update_one_command_contract.rb +79 -0
- data/lib/cuprum/collections/rspec/validate_one_command_contract.rb +96 -0
- data/lib/cuprum/collections/rspec.rb +8 -0
- data/lib/cuprum/collections/version.rb +59 -0
- data/lib/cuprum/collections.rb +26 -0
- metadata +219 -0
@@ -0,0 +1,247 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum/collections'
|
4
|
+
require 'cuprum/collections/queries/ordering'
|
5
|
+
|
6
|
+
module Cuprum::Collections
|
7
|
+
# Abstract base class for collection Query implementations.
|
8
|
+
class Query
|
9
|
+
UNDEFINED = Object.new.freeze
|
10
|
+
private_constant :UNDEFINED
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@criteria = []
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns a normalized representation of the query criteria.
|
17
|
+
#
|
18
|
+
# The query criteria define which data from the collection matches the
|
19
|
+
# query. Specifically, an item in the collection matches the query if and
|
20
|
+
# only if it matches each criterion. If the query has no criteria, then it
|
21
|
+
# will match all items in the collection.
|
22
|
+
#
|
23
|
+
# Each criterion is represented as an Array with three elements:
|
24
|
+
# - The name of the property or column to select by.
|
25
|
+
# - The operation to filter, such as :eq (an equality operation).
|
26
|
+
# - The expected value.
|
27
|
+
#
|
28
|
+
# For example, a query that selects all items whose :series property is
|
29
|
+
# equal to 'The Lord of the Rings' would have the following criterion:
|
30
|
+
# `[:series, :eq, 'The Lord of the Rings']`.
|
31
|
+
#
|
32
|
+
# @return [Array<Array>] the query criteria.
|
33
|
+
#
|
34
|
+
# @see #where
|
35
|
+
def criteria
|
36
|
+
@criteria.dup
|
37
|
+
end
|
38
|
+
|
39
|
+
# Sets or returns the maximum number of items returned by the query.
|
40
|
+
#
|
41
|
+
# @overload limit
|
42
|
+
# @return [Integer, nil] the current limit for the query.
|
43
|
+
#
|
44
|
+
# @overload limit(count)
|
45
|
+
# Returns a copy of the query with the specified limit.
|
46
|
+
#
|
47
|
+
# The query will return at most the specified number of items.
|
48
|
+
#
|
49
|
+
# When #limit is called on a query that already defines a limit, the old
|
50
|
+
# limit is replaced with the new.
|
51
|
+
#
|
52
|
+
# @param count [Integer] the maximum number of items to return.
|
53
|
+
#
|
54
|
+
# @return [Query] the copy of the query.
|
55
|
+
def limit(count = UNDEFINED)
|
56
|
+
return @limit if count == UNDEFINED
|
57
|
+
|
58
|
+
validate_limit(count)
|
59
|
+
|
60
|
+
dup.tap { |copy| copy.with_limit(count) }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Sets or returns the number of ordered items skipped by the query.
|
64
|
+
#
|
65
|
+
# @overload offset
|
66
|
+
# @return [Integer, nil] the current offset for the query.
|
67
|
+
#
|
68
|
+
# @overload offset(count)
|
69
|
+
# Returns a copy of the query with the specified offset.
|
70
|
+
#
|
71
|
+
# The query will skip the specified number of matching items, and return
|
72
|
+
# only matching items after the given offset. If the total number of
|
73
|
+
# matching items is less than or equal to the offset, the query will not
|
74
|
+
# return any items.
|
75
|
+
#
|
76
|
+
# When #offset is called on a query that already defines an offset, the
|
77
|
+
# old offset is replaced with the new.
|
78
|
+
#
|
79
|
+
# @param count [Integer] the number of items to skip.
|
80
|
+
#
|
81
|
+
# @return [Query] the copy of the query.
|
82
|
+
def offset(count = UNDEFINED)
|
83
|
+
return @offset if count == UNDEFINED
|
84
|
+
|
85
|
+
validate_offset(count)
|
86
|
+
|
87
|
+
dup.tap { |copy| copy.with_offset(count) }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns a copy of the query with the specified order.
|
91
|
+
#
|
92
|
+
# The query will find the matching items, sort them in the specified order,
|
93
|
+
# and then apply limit and/or offset (if applicable) to determine the final
|
94
|
+
# returned items.
|
95
|
+
#
|
96
|
+
# When #order is called on a query that already defines an ordering, the old
|
97
|
+
# ordering is replaced with the new.
|
98
|
+
#
|
99
|
+
# @return [Query] the copy of the query.
|
100
|
+
#
|
101
|
+
# @example Sorting By Attribute Names
|
102
|
+
# # This query will sort books by author (ascending), then by title
|
103
|
+
# # (ascending) within authors.
|
104
|
+
# query = query.order(:author, :title)
|
105
|
+
#
|
106
|
+
# @example Sorting With Directions
|
107
|
+
# # This query will sort books by series (ascending), then by the date of
|
108
|
+
# # publication (descending) within series.
|
109
|
+
# query = query.order({ series: :asc, published_at: :desc })
|
110
|
+
#
|
111
|
+
# @overload order
|
112
|
+
# @return [Hash{String,Symbol=>Symbol}] the current ordering for the
|
113
|
+
# query.
|
114
|
+
#
|
115
|
+
# @overload order(*attributes)
|
116
|
+
# Orders the results by the given attributes, ascending, and in the
|
117
|
+
# specified order, i.e. items with the same value of the first attribute
|
118
|
+
# will be sorted by the second (if any), and so on.
|
119
|
+
#
|
120
|
+
# @param attributes [Array<String, Symbol>] The attributes to order by.
|
121
|
+
#
|
122
|
+
# @overload order(attributes)
|
123
|
+
# Orders the results by the given attributes and sort directions, and in
|
124
|
+
# the specified order.
|
125
|
+
#
|
126
|
+
# @param attributes [Hash{String,Symbol=>Symbol}] The attributes to order
|
127
|
+
# by. The hash keys should be the names of attributes or columns, and
|
128
|
+
# the corresponding values should be the sort direction for that
|
129
|
+
# attribute, either :asc or :desc.
|
130
|
+
def order(*attributes)
|
131
|
+
return @order if attributes.empty?
|
132
|
+
|
133
|
+
normalized = Cuprum::Collections::Queries::Ordering.normalize(*attributes)
|
134
|
+
|
135
|
+
dup.tap { |copy| copy.with_order(normalized) }
|
136
|
+
end
|
137
|
+
alias order_by order
|
138
|
+
|
139
|
+
# Returns a copy of the query with no cached query results.
|
140
|
+
#
|
141
|
+
# Once the query has been called (e.g. by calling #each or #to_a), the
|
142
|
+
# matching data is cached. If the underlying collection changes, those
|
143
|
+
# changes will not be reflected in the query.
|
144
|
+
#
|
145
|
+
# Calling #reset clears the cached results. The next time the query is
|
146
|
+
# called, the results will be drawn from the current collection state.
|
147
|
+
#
|
148
|
+
# @return [Cuprum::Collections::Query] a copy of the query with a cleared
|
149
|
+
# results cache.
|
150
|
+
def reset
|
151
|
+
dup.reset!
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns a copy of the query with the specified filters.
|
155
|
+
#
|
156
|
+
# The given parameters are used to construct query criteria, which define
|
157
|
+
# which data from the collection matches the query. Specifically, an item in
|
158
|
+
# the collection matches the query if and only if it matches each criterion.
|
159
|
+
# If the query has no criteria, then it will match all items in the
|
160
|
+
# collection.
|
161
|
+
#
|
162
|
+
# When #where is called on a query that already defines criteria, then the
|
163
|
+
# new criteria are appended to the old. Any items in the collection must
|
164
|
+
# match both the old and the new criteria to be returned by the query.
|
165
|
+
#
|
166
|
+
# @example Filtering Data By Equality
|
167
|
+
# # The query will only return items whose author is 'J.R.R. Tolkien'.
|
168
|
+
# query = query.where { { author: 'J.R.R. Tolkien' } }
|
169
|
+
#
|
170
|
+
# @example Filtering Data By Operator
|
171
|
+
# # The query will only return items whose author is 'J.R.R. Tolkien',
|
172
|
+
# # and whose series is not 'The Lord of the Rings'.
|
173
|
+
# query = query.where do
|
174
|
+
# {
|
175
|
+
# author: eq('J.R.R. Tolkien'),
|
176
|
+
# series: ne('The Lord of the Rings')
|
177
|
+
# }
|
178
|
+
# end
|
179
|
+
#
|
180
|
+
# @overload where(&block)
|
181
|
+
# @yield The given block is passed to a QueryBuilder, which converts the
|
182
|
+
# block to query criteria and generates a new query using those
|
183
|
+
# criteria.
|
184
|
+
#
|
185
|
+
# @yieldreturn [Hash] The filters to apply to the query. The hash keys
|
186
|
+
# should be the names of attributes or columns, and the corresponding
|
187
|
+
# values should be either the literal value for that attribute or a
|
188
|
+
# method call for a valid operation defined for the query.
|
189
|
+
#
|
190
|
+
# @see #criteria
|
191
|
+
def where(filter = nil, strategy: nil, &block)
|
192
|
+
filter ||= block
|
193
|
+
|
194
|
+
return dup if filter.nil? && strategy.nil?
|
195
|
+
|
196
|
+
query_builder.call(strategy: strategy, where: filter)
|
197
|
+
end
|
198
|
+
|
199
|
+
protected
|
200
|
+
|
201
|
+
def reset!
|
202
|
+
# :nocov:
|
203
|
+
self
|
204
|
+
# :nocov:
|
205
|
+
end
|
206
|
+
|
207
|
+
def with_criteria(criteria)
|
208
|
+
@criteria += criteria
|
209
|
+
|
210
|
+
self
|
211
|
+
end
|
212
|
+
|
213
|
+
def with_limit(count)
|
214
|
+
@limit = count
|
215
|
+
|
216
|
+
self
|
217
|
+
end
|
218
|
+
|
219
|
+
def with_offset(count)
|
220
|
+
@offset = count
|
221
|
+
|
222
|
+
self
|
223
|
+
end
|
224
|
+
|
225
|
+
def with_order(order)
|
226
|
+
@order = order
|
227
|
+
|
228
|
+
self
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
|
233
|
+
def validate_limit(count)
|
234
|
+
return if count.is_a?(Integer) && !count.negative?
|
235
|
+
|
236
|
+
raise ArgumentError, 'limit must be a non-negative integer', caller(1..-1)
|
237
|
+
end
|
238
|
+
|
239
|
+
def validate_offset(count)
|
240
|
+
return if count.is_a?(Integer) && !count.negative?
|
241
|
+
|
242
|
+
raise ArgumentError,
|
243
|
+
'offset must be a non-negative integer',
|
244
|
+
caller(1..-1)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum/collections'
|
4
|
+
require 'cuprum/collections/queries/parse'
|
5
|
+
|
6
|
+
module Cuprum::Collections
|
7
|
+
# Internal class that handles parsing and applying criteria to a query.
|
8
|
+
class QueryBuilder
|
9
|
+
# Exception class to be raised when the query cannot be parsed.
|
10
|
+
class ParseError < RuntimeError; end
|
11
|
+
|
12
|
+
# @param base_query [Cuprum::Collections::Query] The original query.
|
13
|
+
def initialize(base_query)
|
14
|
+
@base_query = base_query
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Cuprum::Collections::Query] the original query.
|
18
|
+
attr_reader :base_query
|
19
|
+
|
20
|
+
# Returns a copy of the query updated with the generated criteria.
|
21
|
+
#
|
22
|
+
# Classifies the parameters to determine parsing strategy, then uses that
|
23
|
+
# strategy to parse the parameters into an array of criteria. Then, copies
|
24
|
+
# the original query and updates the copy with the parsed criteria.
|
25
|
+
#
|
26
|
+
# @param strategy [Symbol, nil] The specified strategy for parsing the given
|
27
|
+
# filter into criteria. If nil, the builder will attempt to guess the
|
28
|
+
# strategy based on the given filter.
|
29
|
+
# @param where [Object] The filter used to match items in the collection.
|
30
|
+
#
|
31
|
+
# @return [Cuprum::Collections::Query] the copied and updated query.
|
32
|
+
def call(where:, strategy: nil)
|
33
|
+
criteria =
|
34
|
+
if strategy == :unsafe
|
35
|
+
where
|
36
|
+
else
|
37
|
+
parse_criteria(strategy: strategy, where: where)
|
38
|
+
end
|
39
|
+
|
40
|
+
build_query(criteria)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def build_query(criteria)
|
46
|
+
base_query
|
47
|
+
.dup
|
48
|
+
.send(:with_criteria, criteria)
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_criteria(strategy:, where:)
|
52
|
+
result = Cuprum::Collections::Queries::Parse
|
53
|
+
.new
|
54
|
+
.call(strategy: strategy, where: where)
|
55
|
+
|
56
|
+
return result.value if result.success?
|
57
|
+
|
58
|
+
raise ParseError, result.error.message
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/presence'
|
4
|
+
require 'stannum/constraints/types/hash_with_indifferent_keys'
|
5
|
+
require 'stannum/rspec/validate_parameter'
|
6
|
+
|
7
|
+
require 'cuprum/collections/rspec'
|
8
|
+
|
9
|
+
module Cuprum::Collections::RSpec
|
10
|
+
# Contract validating the behavior of an Assign command implementation.
|
11
|
+
ASSIGN_ONE_COMMAND_CONTRACT = lambda do |allow_extra_attributes:|
|
12
|
+
describe '#call' do
|
13
|
+
shared_examples 'should assign the attributes' do
|
14
|
+
it { expect(result).to be_a_passing_result }
|
15
|
+
|
16
|
+
it { expect(result.value).to be_a entity.class }
|
17
|
+
|
18
|
+
it { expect(result.value).to be == expected_value }
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:attributes) { {} }
|
22
|
+
let(:result) { command.call(attributes: attributes, entity: entity) }
|
23
|
+
let(:expected_attributes) do
|
24
|
+
initial_attributes.merge(attributes)
|
25
|
+
end
|
26
|
+
let(:expected_value) do
|
27
|
+
defined?(super()) ? super() : expected_attributes
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should validate the :attributes keyword' do
|
31
|
+
expect(command)
|
32
|
+
.to validate_parameter(:call, :attributes)
|
33
|
+
.using_constraint(
|
34
|
+
Stannum::Constraints::Types::HashWithIndifferentKeys.new
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should validate the :entity keyword' do
|
39
|
+
expect(command)
|
40
|
+
.to validate_parameter(:call, :entity)
|
41
|
+
.using_constraint(entity_type)
|
42
|
+
.with_parameters(attributes: {}, entity: nil)
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'with an empty attributes hash' do
|
46
|
+
let(:attributes) { {} }
|
47
|
+
|
48
|
+
include_examples 'should assign the attributes'
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'with an attributes hash with partial attributes' do
|
52
|
+
let(:attributes) { { title: 'Gideon the Ninth' } }
|
53
|
+
|
54
|
+
include_examples 'should assign the attributes'
|
55
|
+
end
|
56
|
+
|
57
|
+
describe 'with an attributes hash with full attributes' do
|
58
|
+
let(:attributes) do
|
59
|
+
{
|
60
|
+
title: 'Gideon the Ninth',
|
61
|
+
author: 'Tammsyn Muir',
|
62
|
+
series: 'The Locked Tomb',
|
63
|
+
category: 'Horror'
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
include_examples 'should assign the attributes'
|
68
|
+
end
|
69
|
+
|
70
|
+
describe 'with an attributes hash with extra attributes' do
|
71
|
+
let(:attributes) do
|
72
|
+
{
|
73
|
+
title: 'The Book of Lost Tales',
|
74
|
+
audiobook: true
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
if allow_extra_attributes
|
79
|
+
include_examples 'should assign the attributes'
|
80
|
+
else
|
81
|
+
# :nocov:
|
82
|
+
let(:valid_attributes) do
|
83
|
+
defined?(super()) ? super() : expected_attributes.keys
|
84
|
+
end
|
85
|
+
let(:expected_error) do
|
86
|
+
Cuprum::Collections::Errors::ExtraAttributes.new(
|
87
|
+
entity_class: entity.class,
|
88
|
+
extra_attributes: %w[audiobook],
|
89
|
+
valid_attributes: valid_attributes
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should return a failing result' do
|
94
|
+
expect(result).to be_a_failing_result.with_error(expected_error)
|
95
|
+
end
|
96
|
+
# :nocov:
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'when the entity has existing attributes' do
|
101
|
+
let(:initial_attributes) do
|
102
|
+
# :nocov:
|
103
|
+
if defined?(super())
|
104
|
+
super().merge(fixtures_data.first)
|
105
|
+
else
|
106
|
+
fixtures_data.first
|
107
|
+
end
|
108
|
+
# :nocov:
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'with an empty attributes hash' do
|
112
|
+
let(:attributes) { {} }
|
113
|
+
|
114
|
+
include_examples 'should assign the attributes'
|
115
|
+
end
|
116
|
+
|
117
|
+
describe 'with an attributes hash with partial attributes' do
|
118
|
+
let(:attributes) { { title: 'Gideon the Ninth' } }
|
119
|
+
|
120
|
+
include_examples 'should assign the attributes'
|
121
|
+
end
|
122
|
+
|
123
|
+
describe 'with an attributes hash with full attributes' do
|
124
|
+
let(:attributes) do
|
125
|
+
{
|
126
|
+
title: 'Gideon the Ninth',
|
127
|
+
author: 'Tammsyn Muir',
|
128
|
+
series: 'The Locked Tomb',
|
129
|
+
category: 'Horror'
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
include_examples 'should assign the attributes'
|
134
|
+
end
|
135
|
+
|
136
|
+
describe 'with an attributes hash with extra attributes' do
|
137
|
+
let(:attributes) do
|
138
|
+
{
|
139
|
+
title: 'The Book of Lost Tales',
|
140
|
+
audiobook: true
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
if allow_extra_attributes
|
145
|
+
include_examples 'should assign the attributes'
|
146
|
+
else
|
147
|
+
# :nocov:
|
148
|
+
let(:valid_attributes) do
|
149
|
+
defined?(super()) ? super() : expected_attributes.keys
|
150
|
+
end
|
151
|
+
let(:expected_error) do
|
152
|
+
Cuprum::Collections::Errors::ExtraAttributes.new(
|
153
|
+
entity_class: entity.class,
|
154
|
+
extra_attributes: %w[audiobook],
|
155
|
+
valid_attributes: valid_attributes
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'should return a failing result' do
|
160
|
+
expect(result).to be_a_failing_result.with_error(expected_error)
|
161
|
+
end
|
162
|
+
# :nocov:
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/types/hash_with_indifferent_keys'
|
4
|
+
require 'stannum/rspec/validate_parameter'
|
5
|
+
|
6
|
+
require 'cuprum/collections/rspec'
|
7
|
+
|
8
|
+
module Cuprum::Collections::RSpec
|
9
|
+
# Contract validating the behavior of a Build command implementation.
|
10
|
+
BUILD_ONE_COMMAND_CONTRACT = lambda do |allow_extra_attributes:|
|
11
|
+
include Stannum::RSpec::Matchers
|
12
|
+
|
13
|
+
describe '#call' do
|
14
|
+
shared_examples 'should build the entity' do
|
15
|
+
it { expect(result).to be_a_passing_result }
|
16
|
+
|
17
|
+
it { expect(result.value).to be == expected_value }
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:attributes) { {} }
|
21
|
+
let(:result) { command.call(attributes: attributes) }
|
22
|
+
let(:expected_attributes) do
|
23
|
+
attributes
|
24
|
+
end
|
25
|
+
let(:expected_value) do
|
26
|
+
defined?(super()) ? super() : attributes
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should validate the :attributes keyword' do
|
30
|
+
expect(command)
|
31
|
+
.to validate_parameter(:call, :attributes)
|
32
|
+
.using_constraint(
|
33
|
+
Stannum::Constraints::Types::HashWithIndifferentKeys.new
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'with an empty attributes hash' do
|
38
|
+
let(:attributes) { {} }
|
39
|
+
|
40
|
+
include_examples 'should build the entity'
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'with an attributes hash with partial attributes' do
|
44
|
+
let(:attributes) { { title: 'Gideon the Ninth' } }
|
45
|
+
|
46
|
+
include_examples 'should build the entity'
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'with an attributes hash with full attributes' do
|
50
|
+
let(:attributes) do
|
51
|
+
{
|
52
|
+
title: 'Gideon the Ninth',
|
53
|
+
author: 'Tammsyn Muir',
|
54
|
+
series: 'The Locked Tomb',
|
55
|
+
category: 'Horror'
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
include_examples 'should build the entity'
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'with an attributes hash with extra attributes' do
|
63
|
+
let(:attributes) do
|
64
|
+
{
|
65
|
+
title: 'The Book of Lost Tales',
|
66
|
+
audiobook: true
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
if allow_extra_attributes
|
71
|
+
include_examples 'should build the entity'
|
72
|
+
else
|
73
|
+
# :nocov:
|
74
|
+
let(:valid_attributes) do
|
75
|
+
defined?(super()) ? super() : expected_attributes.keys
|
76
|
+
end
|
77
|
+
let(:expected_error) do
|
78
|
+
Cuprum::Collections::Errors::ExtraAttributes.new(
|
79
|
+
entity_class: entity_type,
|
80
|
+
extra_attributes: %w[audiobook],
|
81
|
+
valid_attributes: valid_attributes
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should return a failing result' do
|
86
|
+
expect(result).to be_a_failing_result.with_error(expected_error)
|
87
|
+
end
|
88
|
+
# :nocov:
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|