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