cuprum-collections 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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