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,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/types/hash_with_string_keys'
|
4
|
+
|
5
|
+
require 'cuprum/collections/basic/command'
|
6
|
+
require 'cuprum/collections/basic/commands'
|
7
|
+
require 'cuprum/collections/errors/already_exists'
|
8
|
+
|
9
|
+
module Cuprum::Collections::Basic::Commands
|
10
|
+
# Command for inserting an entity into the collection.
|
11
|
+
class InsertOne < Cuprum::Collections::Basic::Command
|
12
|
+
# @!method call(entity:)
|
13
|
+
# Inserts the entity into the collection.
|
14
|
+
#
|
15
|
+
# If the collection already includes an entity with the same primary key,
|
16
|
+
# #call will fail and the collection will not be updated.
|
17
|
+
#
|
18
|
+
# @param entity [Hash] The collection entity to persist.
|
19
|
+
#
|
20
|
+
# @return [Cuprum::Result<Hash>] the persisted entity.
|
21
|
+
validate_parameters :call do
|
22
|
+
keyword :entity,
|
23
|
+
Stannum::Constraints::Types::HashWithStringKeys.new
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def find_existing(entity:)
|
29
|
+
value = entity[primary_key_name.to_s]
|
30
|
+
index = data.index { |item| item[primary_key_name.to_s] == value }
|
31
|
+
|
32
|
+
return if index.nil?
|
33
|
+
|
34
|
+
error = Cuprum::Collections::Errors::AlreadyExists.new(
|
35
|
+
collection_name: collection_name,
|
36
|
+
primary_key_name: primary_key_name,
|
37
|
+
primary_key_values: value
|
38
|
+
)
|
39
|
+
failure(error)
|
40
|
+
end
|
41
|
+
|
42
|
+
def process(entity:)
|
43
|
+
step { find_existing(entity: entity) }
|
44
|
+
|
45
|
+
data << tools.hash_tools.deep_dup(entity)
|
46
|
+
|
47
|
+
entity
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/types/hash_with_string_keys'
|
4
|
+
|
5
|
+
require 'cuprum/collections/basic/command'
|
6
|
+
require 'cuprum/collections/basic/commands'
|
7
|
+
require 'cuprum/collections/errors/not_found'
|
8
|
+
|
9
|
+
module Cuprum::Collections::Basic::Commands
|
10
|
+
# Command for updating an entity in the collection.
|
11
|
+
class UpdateOne < Cuprum::Collections::Basic::Command
|
12
|
+
# @!method call(entity:)
|
13
|
+
# Updates the entity in the collection.
|
14
|
+
#
|
15
|
+
# If the collection does not already have an entity with the same primary
|
16
|
+
# key, #call will fail and the collection will not be updated.
|
17
|
+
#
|
18
|
+
# @param entity [Hash] The collection entity to persist.
|
19
|
+
#
|
20
|
+
# @return [Cuprum::Result<Hash>] the persisted entity.
|
21
|
+
validate_parameters :call do
|
22
|
+
keyword :entity,
|
23
|
+
Stannum::Constraints::Types::HashWithStringKeys.new
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def find_existing(entity:)
|
29
|
+
value = entity[primary_key_name.to_s]
|
30
|
+
index = data.index { |item| item[primary_key_name.to_s] == value }
|
31
|
+
|
32
|
+
return index unless index.nil?
|
33
|
+
|
34
|
+
error = Cuprum::Collections::Errors::NotFound.new(
|
35
|
+
collection_name: collection_name,
|
36
|
+
primary_key_name: primary_key_name,
|
37
|
+
primary_key_values: entity[primary_key_name.to_s]
|
38
|
+
)
|
39
|
+
failure(error)
|
40
|
+
end
|
41
|
+
|
42
|
+
def process(entity:)
|
43
|
+
index = step { find_existing(entity: entity) }
|
44
|
+
|
45
|
+
entity = data[index].merge(entity)
|
46
|
+
|
47
|
+
data[index] = entity.dup
|
48
|
+
|
49
|
+
entity
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/types/hash_with_string_keys'
|
4
|
+
|
5
|
+
require 'cuprum/collections/basic/command'
|
6
|
+
require 'cuprum/collections/basic/commands'
|
7
|
+
require 'cuprum/collections/errors/failed_validation'
|
8
|
+
require 'cuprum/collections/errors/missing_default_contract'
|
9
|
+
|
10
|
+
module Cuprum::Collections::Basic::Commands
|
11
|
+
# Command for validating a collection entity.
|
12
|
+
class ValidateOne < Cuprum::Collections::Basic::Command
|
13
|
+
# @!method call(entity:, contract: nil)
|
14
|
+
# Validates the entity against the given or default contract.
|
15
|
+
#
|
16
|
+
# If the entity matches the contract, #call will return a passing result
|
17
|
+
# with the entity as the result value. If the entity does not match the
|
18
|
+
# contract, #call will return a failing result with a FailedValidation
|
19
|
+
# error and the validation errors.
|
20
|
+
#
|
21
|
+
# @param contract [Stannum::Constraints:Base] The contract with which to
|
22
|
+
# validate the entity. If not given, the entity will be validated using
|
23
|
+
# the collection's default contract.
|
24
|
+
# @param entity [Hash] The collection entity to validate.
|
25
|
+
#
|
26
|
+
# @return [Cuprum::Result<Hash>] the validated entity.
|
27
|
+
validate_parameters :call do
|
28
|
+
keyword :contract,
|
29
|
+
Stannum::Constraints::Base,
|
30
|
+
optional: true
|
31
|
+
keyword :entity,
|
32
|
+
Stannum::Constraints::Types::HashWithStringKeys.new
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def contract_or_default(contract:, entity:)
|
38
|
+
return contract if contract
|
39
|
+
|
40
|
+
return default_contract if default_contract
|
41
|
+
|
42
|
+
error = Cuprum::Collections::Errors::MissingDefaultContract.new(
|
43
|
+
entity_class: entity.class
|
44
|
+
)
|
45
|
+
failure(error)
|
46
|
+
end
|
47
|
+
|
48
|
+
def process(entity:, contract: nil)
|
49
|
+
contract =
|
50
|
+
step { contract_or_default(contract: contract, entity: entity) }
|
51
|
+
|
52
|
+
step { validate_entity(contract: contract, entity: entity) }
|
53
|
+
|
54
|
+
entity
|
55
|
+
end
|
56
|
+
|
57
|
+
def validate_entity(contract:, entity:)
|
58
|
+
valid, errors = contract.match(entity)
|
59
|
+
|
60
|
+
return if valid
|
61
|
+
|
62
|
+
error = Cuprum::Collections::Errors::FailedValidation.new(
|
63
|
+
entity_class: entity.class,
|
64
|
+
errors: errors
|
65
|
+
)
|
66
|
+
failure(error)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum/collections/basic'
|
4
|
+
|
5
|
+
module Cuprum::Collections::Basic
|
6
|
+
# Namespace for commands implementing basic collection functionality.
|
7
|
+
module Commands
|
8
|
+
autoload :AssignOne, 'cuprum/collections/basic/commands/assign_one'
|
9
|
+
autoload :BuildOne, 'cuprum/collections/basic/commands/build_one'
|
10
|
+
autoload :DestroyOne, 'cuprum/collections/basic/commands/destroy_one'
|
11
|
+
autoload :FindMany, 'cuprum/collections/basic/commands/find_many'
|
12
|
+
autoload :FindMatching, 'cuprum/collections/basic/commands/find_matching'
|
13
|
+
autoload :FindOne, 'cuprum/collections/basic/commands/find_one'
|
14
|
+
autoload :InsertOne, 'cuprum/collections/basic/commands/insert_one'
|
15
|
+
autoload :UpdateOne, 'cuprum/collections/basic/commands/update_one'
|
16
|
+
autoload :ValidateOne, 'cuprum/collections/basic/commands/validate_one'
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum/collections/basic'
|
4
|
+
require 'cuprum/collections/basic/query_builder'
|
5
|
+
require 'cuprum/collections/query'
|
6
|
+
|
7
|
+
module Cuprum::Collections::Basic
|
8
|
+
# Concrete implementation of a Query for an in-memory collection.
|
9
|
+
class Query < Cuprum::Collections::Query
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
# @param data [Array<Hash>] The current data in the collection. Should be an
|
13
|
+
# Array of Hashes, each of which represents one item in the collection.
|
14
|
+
def initialize(data)
|
15
|
+
super()
|
16
|
+
|
17
|
+
@data = data
|
18
|
+
@filters = []
|
19
|
+
@limit = nil
|
20
|
+
@offset = nil
|
21
|
+
@order = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Iterates through the collection, yielding each item matching the query.
|
25
|
+
#
|
26
|
+
# If the query has criteria, only items matching each criterion will be
|
27
|
+
# processed; these are the matching items. If the query does not have any
|
28
|
+
# criteria, all items in the collection will be processed.
|
29
|
+
#
|
30
|
+
# If the query has an ordering, the matching items are then sorted in the
|
31
|
+
# specified order. If the query does not have an order, the matching items
|
32
|
+
# will be processed in the order they appear in the collection.
|
33
|
+
#
|
34
|
+
# Finally, the limit and/or offset will be applied to the sorted matching
|
35
|
+
# items. Each sorted, matching item starting at the offset and up to the
|
36
|
+
# given limit of items will be yielded to the block.
|
37
|
+
#
|
38
|
+
# @overload each
|
39
|
+
# @return [Enumerator] an enumerator that iterates over the sorted,
|
40
|
+
# matching items within the given offset and limit.
|
41
|
+
#
|
42
|
+
# @overload each(&block)
|
43
|
+
# @yield [Object] Each sorted, matching item within the given offset and
|
44
|
+
# limit is yielded to the block.
|
45
|
+
#
|
46
|
+
# @see #limit
|
47
|
+
# @see #offset
|
48
|
+
# @see #order
|
49
|
+
# @see #to_a
|
50
|
+
# @see #where
|
51
|
+
def each(&block)
|
52
|
+
return enum_for(:each) unless block_given?
|
53
|
+
|
54
|
+
filtered_data.each(&block)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Checks for the presence of collection items matching the query.
|
58
|
+
#
|
59
|
+
# If the query has criteria, then only items matching each criterion will be
|
60
|
+
# processed; these are the matching items. If there is at least one matching
|
61
|
+
# item, #exists will return true; otherwise, it will return false.
|
62
|
+
#
|
63
|
+
# @return [Boolean] true if any items match the query; otherwise false.
|
64
|
+
def exists?
|
65
|
+
data.any? do |item|
|
66
|
+
@filters.all? { |filter| filter.call(item) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns an array containing each collection item matching the query.
|
71
|
+
#
|
72
|
+
# If the query has criteria, only items matching each criterion will be
|
73
|
+
# processed; these are the matching items. If the query does not have any
|
74
|
+
# criteria, all items in the collection will be processed.
|
75
|
+
#
|
76
|
+
# If the query has an ordering, the matching items are then sorted in the
|
77
|
+
# specified order. If the query does not have an order, the matching items
|
78
|
+
# will be processed in the order they appear in the collection.
|
79
|
+
#
|
80
|
+
# Finally, the limit and/or offset will be applied to the sorted matching
|
81
|
+
# items. Each sorted, matching item starting at the offset and up to the
|
82
|
+
# given limit of items will be returned in the array.
|
83
|
+
#
|
84
|
+
# @return [Array] The sorted, matching items within the given offset and
|
85
|
+
# limit.
|
86
|
+
#
|
87
|
+
# @see #each
|
88
|
+
# @see #limit
|
89
|
+
# @see #offset
|
90
|
+
# @see #order
|
91
|
+
# @see #where
|
92
|
+
def to_a
|
93
|
+
filtered_data
|
94
|
+
end
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
def query_builder
|
99
|
+
Cuprum::Collections::Basic::QueryBuilder.new(self)
|
100
|
+
end
|
101
|
+
|
102
|
+
def reset!
|
103
|
+
@filtered_data = nil
|
104
|
+
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
def with_filters(filters)
|
109
|
+
@filters += filters
|
110
|
+
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
attr_reader :data
|
117
|
+
|
118
|
+
attr_reader :filters
|
119
|
+
|
120
|
+
def apply_filters(data)
|
121
|
+
data.select do |item|
|
122
|
+
@filters.all? { |filter| filter.call(item) }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def apply_limit_offset(data)
|
127
|
+
return data[@offset...(@offset + @limit)] || [] if @limit && @offset
|
128
|
+
return data[0...@limit] if @limit
|
129
|
+
|
130
|
+
return data[@offset..] || [] if @offset
|
131
|
+
|
132
|
+
data
|
133
|
+
end
|
134
|
+
|
135
|
+
def apply_order(data)
|
136
|
+
return data if @order.empty?
|
137
|
+
|
138
|
+
data.sort do |u, v|
|
139
|
+
@order.reduce(0) do |memo, (attribute, direction)|
|
140
|
+
next memo unless memo.zero?
|
141
|
+
|
142
|
+
attr_name = attribute.to_s
|
143
|
+
|
144
|
+
cmp = u[attr_name] <=> v[attr_name]
|
145
|
+
|
146
|
+
direction == :asc ? cmp : -cmp
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def filtered_data
|
152
|
+
@filtered_data ||=
|
153
|
+
data
|
154
|
+
.yield_self { |ary| apply_filters(ary) }
|
155
|
+
.yield_self { |ary| apply_order(ary) }
|
156
|
+
.yield_self { |ary| apply_limit_offset(ary) }
|
157
|
+
.map(&:dup)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum/collections/basic'
|
4
|
+
require 'cuprum/collections/query_builder'
|
5
|
+
|
6
|
+
module Cuprum::Collections::Basic
|
7
|
+
# Concrete implementation of QueryBuilder for a basic query.
|
8
|
+
class QueryBuilder < Cuprum::Collections::QueryBuilder
|
9
|
+
# @param base_query [Cuprum::Collections::Basic::Query] The original
|
10
|
+
# query.
|
11
|
+
def initialize(base_query)
|
12
|
+
super
|
13
|
+
|
14
|
+
@filters = base_query.send(:filters)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :filters
|
20
|
+
|
21
|
+
def build_filters(criteria)
|
22
|
+
criteria.map do |(attribute, operator, value)|
|
23
|
+
send(operator, attribute, value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_query(criteria)
|
28
|
+
super.send(:with_filters, build_filters(criteria))
|
29
|
+
end
|
30
|
+
|
31
|
+
def equal(attribute, value)
|
32
|
+
->(actual) { actual[attribute.to_s] == value }
|
33
|
+
end
|
34
|
+
alias eq equal
|
35
|
+
|
36
|
+
def greater_than(attribute, value)
|
37
|
+
->(actual) { actual[attribute.to_s] > value }
|
38
|
+
end
|
39
|
+
alias gt greater_than
|
40
|
+
|
41
|
+
def greater_than_or_equal_to(attribute, value)
|
42
|
+
->(actual) { actual[attribute.to_s] >= value }
|
43
|
+
end
|
44
|
+
alias gte greater_than_or_equal_to
|
45
|
+
|
46
|
+
def less_than(attribute, value)
|
47
|
+
->(actual) { actual[attribute.to_s] < value }
|
48
|
+
end
|
49
|
+
alias lt less_than
|
50
|
+
|
51
|
+
def less_than_or_equal_to(attribute, value)
|
52
|
+
->(actual) { actual[attribute.to_s] <= value }
|
53
|
+
end
|
54
|
+
alias lte less_than_or_equal_to
|
55
|
+
|
56
|
+
def not_equal(attribute, value)
|
57
|
+
->(actual) { actual[attribute.to_s] != value }
|
58
|
+
end
|
59
|
+
alias ne not_equal
|
60
|
+
|
61
|
+
def not_one_of(attribute, value)
|
62
|
+
->(actual) { !value.include?(actual[attribute.to_s]) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def one_of(attribute, value)
|
66
|
+
->(actual) { value.include?(actual[attribute.to_s]) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|