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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +59 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/DEVELOPMENT.md +25 -0
  5. data/LICENSE +22 -0
  6. data/README.md +950 -0
  7. data/lib/cuprum/collections/base.rb +11 -0
  8. data/lib/cuprum/collections/basic/collection.rb +135 -0
  9. data/lib/cuprum/collections/basic/command.rb +112 -0
  10. data/lib/cuprum/collections/basic/commands/assign_one.rb +54 -0
  11. data/lib/cuprum/collections/basic/commands/build_one.rb +45 -0
  12. data/lib/cuprum/collections/basic/commands/destroy_one.rb +48 -0
  13. data/lib/cuprum/collections/basic/commands/find_many.rb +65 -0
  14. data/lib/cuprum/collections/basic/commands/find_matching.rb +126 -0
  15. data/lib/cuprum/collections/basic/commands/find_one.rb +49 -0
  16. data/lib/cuprum/collections/basic/commands/insert_one.rb +50 -0
  17. data/lib/cuprum/collections/basic/commands/update_one.rb +52 -0
  18. data/lib/cuprum/collections/basic/commands/validate_one.rb +69 -0
  19. data/lib/cuprum/collections/basic/commands.rb +18 -0
  20. data/lib/cuprum/collections/basic/query.rb +160 -0
  21. data/lib/cuprum/collections/basic/query_builder.rb +69 -0
  22. data/lib/cuprum/collections/basic/rspec/command_contract.rb +392 -0
  23. data/lib/cuprum/collections/basic/rspec.rb +8 -0
  24. data/lib/cuprum/collections/basic.rb +22 -0
  25. data/lib/cuprum/collections/command.rb +26 -0
  26. data/lib/cuprum/collections/commands/abstract_find_many.rb +77 -0
  27. data/lib/cuprum/collections/commands/abstract_find_matching.rb +64 -0
  28. data/lib/cuprum/collections/commands/abstract_find_one.rb +44 -0
  29. data/lib/cuprum/collections/commands.rb +8 -0
  30. data/lib/cuprum/collections/constraints/attribute_name.rb +22 -0
  31. data/lib/cuprum/collections/constraints/order/attributes_array.rb +26 -0
  32. data/lib/cuprum/collections/constraints/order/attributes_hash.rb +27 -0
  33. data/lib/cuprum/collections/constraints/order/complex_ordering.rb +46 -0
  34. data/lib/cuprum/collections/constraints/order/sort_direction.rb +32 -0
  35. data/lib/cuprum/collections/constraints/order.rb +8 -0
  36. data/lib/cuprum/collections/constraints/ordering.rb +114 -0
  37. data/lib/cuprum/collections/constraints/query_hash.rb +25 -0
  38. data/lib/cuprum/collections/constraints.rb +8 -0
  39. data/lib/cuprum/collections/errors/already_exists.rb +86 -0
  40. data/lib/cuprum/collections/errors/extra_attributes.rb +66 -0
  41. data/lib/cuprum/collections/errors/failed_validation.rb +66 -0
  42. data/lib/cuprum/collections/errors/invalid_parameters.rb +50 -0
  43. data/lib/cuprum/collections/errors/invalid_query.rb +55 -0
  44. data/lib/cuprum/collections/errors/missing_default_contract.rb +49 -0
  45. data/lib/cuprum/collections/errors/not_found.rb +81 -0
  46. data/lib/cuprum/collections/errors/unknown_operator.rb +71 -0
  47. data/lib/cuprum/collections/errors.rb +8 -0
  48. data/lib/cuprum/collections/queries/ordering.rb +74 -0
  49. data/lib/cuprum/collections/queries/parse.rb +22 -0
  50. data/lib/cuprum/collections/queries/parse_block.rb +206 -0
  51. data/lib/cuprum/collections/queries/parse_strategy.rb +91 -0
  52. data/lib/cuprum/collections/queries.rb +25 -0
  53. data/lib/cuprum/collections/query.rb +247 -0
  54. data/lib/cuprum/collections/query_builder.rb +61 -0
  55. data/lib/cuprum/collections/rspec/assign_one_command_contract.rb +168 -0
  56. data/lib/cuprum/collections/rspec/build_one_command_contract.rb +93 -0
  57. data/lib/cuprum/collections/rspec/collection_contract.rb +153 -0
  58. data/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +106 -0
  59. data/lib/cuprum/collections/rspec/find_many_command_contract.rb +327 -0
  60. data/lib/cuprum/collections/rspec/find_matching_command_contract.rb +194 -0
  61. data/lib/cuprum/collections/rspec/find_one_command_contract.rb +154 -0
  62. data/lib/cuprum/collections/rspec/fixtures.rb +89 -0
  63. data/lib/cuprum/collections/rspec/insert_one_command_contract.rb +83 -0
  64. data/lib/cuprum/collections/rspec/query_builder_contract.rb +92 -0
  65. data/lib/cuprum/collections/rspec/query_contract.rb +650 -0
  66. data/lib/cuprum/collections/rspec/querying_contract.rb +298 -0
  67. data/lib/cuprum/collections/rspec/update_one_command_contract.rb +79 -0
  68. data/lib/cuprum/collections/rspec/validate_one_command_contract.rb +96 -0
  69. data/lib/cuprum/collections/rspec.rb +8 -0
  70. data/lib/cuprum/collections/version.rb +59 -0
  71. data/lib/cuprum/collections.rb +26 -0
  72. 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