cuprum-collections 0.1.0 → 0.3.0.rc.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +321 -15
  4. data/lib/cuprum/collections/basic/collection.rb +13 -0
  5. data/lib/cuprum/collections/basic/commands/destroy_one.rb +4 -3
  6. data/lib/cuprum/collections/basic/commands/find_many.rb +1 -1
  7. data/lib/cuprum/collections/basic/commands/insert_one.rb +4 -3
  8. data/lib/cuprum/collections/basic/commands/update_one.rb +4 -3
  9. data/lib/cuprum/collections/basic/query.rb +3 -3
  10. data/lib/cuprum/collections/basic/repository.rb +67 -0
  11. data/lib/cuprum/collections/commands/abstract_find_many.rb +33 -32
  12. data/lib/cuprum/collections/commands/abstract_find_one.rb +4 -3
  13. data/lib/cuprum/collections/commands/create.rb +60 -0
  14. data/lib/cuprum/collections/commands/find_one_matching.rb +134 -0
  15. data/lib/cuprum/collections/commands/update.rb +74 -0
  16. data/lib/cuprum/collections/commands/upsert.rb +162 -0
  17. data/lib/cuprum/collections/commands.rb +7 -2
  18. data/lib/cuprum/collections/errors/abstract_find_error.rb +210 -0
  19. data/lib/cuprum/collections/errors/already_exists.rb +4 -72
  20. data/lib/cuprum/collections/errors/extra_attributes.rb +8 -18
  21. data/lib/cuprum/collections/errors/failed_validation.rb +5 -18
  22. data/lib/cuprum/collections/errors/invalid_parameters.rb +7 -15
  23. data/lib/cuprum/collections/errors/invalid_query.rb +5 -15
  24. data/lib/cuprum/collections/errors/missing_default_contract.rb +5 -17
  25. data/lib/cuprum/collections/errors/not_found.rb +4 -67
  26. data/lib/cuprum/collections/errors/not_unique.rb +18 -0
  27. data/lib/cuprum/collections/errors/unknown_operator.rb +7 -17
  28. data/lib/cuprum/collections/errors.rb +13 -1
  29. data/lib/cuprum/collections/queries/ordering.rb +4 -2
  30. data/lib/cuprum/collections/repository.rb +105 -0
  31. data/lib/cuprum/collections/rspec/assign_one_command_contract.rb +2 -2
  32. data/lib/cuprum/collections/rspec/build_one_command_contract.rb +1 -1
  33. data/lib/cuprum/collections/rspec/collection_contract.rb +140 -103
  34. data/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +8 -6
  35. data/lib/cuprum/collections/rspec/find_many_command_contract.rb +114 -34
  36. data/lib/cuprum/collections/rspec/find_one_command_contract.rb +12 -9
  37. data/lib/cuprum/collections/rspec/insert_one_command_contract.rb +4 -3
  38. data/lib/cuprum/collections/rspec/query_contract.rb +3 -3
  39. data/lib/cuprum/collections/rspec/querying_contract.rb +2 -2
  40. data/lib/cuprum/collections/rspec/repository_contract.rb +235 -0
  41. data/lib/cuprum/collections/rspec/update_one_command_contract.rb +4 -3
  42. data/lib/cuprum/collections/version.rb +3 -3
  43. data/lib/cuprum/collections.rb +1 -0
  44. metadata +25 -91
@@ -17,39 +17,41 @@ module Cuprum::Collections::Commands
17
17
  (scope || build_query).where { { key => one_of(primary_keys) } }
18
18
  end
19
19
 
20
- def handle_missing_items(allow_partial:, items:, primary_keys:)
21
- found, missing = match_items(items: items, primary_keys: primary_keys)
20
+ def build_results(items:, primary_keys:)
21
+ primary_keys.map do |primary_key_value|
22
+ next success(items[primary_key_value]) if items.key?(primary_key_value)
22
23
 
23
- return found if missing.empty?
24
+ failure(not_found_error(primary_key_value))
25
+ end
26
+ end
27
+
28
+ def build_result_list(results, allow_partial:, envelope:)
29
+ unless envelope
30
+ return Cuprum::ResultList.new(*results, allow_partial: allow_partial)
31
+ end
24
32
 
25
- return found if allow_partial && !found.empty?
33
+ value = envelope ? wrap_items(results.map(&:value)) : nil
26
34
 
27
- error = Cuprum::Collections::Errors::NotFound.new(
28
- collection_name: collection_name,
29
- primary_key_name: primary_key_name,
30
- primary_key_values: missing
35
+ Cuprum::ResultList.new(
36
+ *results,
37
+ allow_partial: allow_partial,
38
+ value: value
31
39
  )
32
- Cuprum::Result.new(error: error)
33
40
  end
34
41
 
35
42
  def items_with_primary_keys(items:)
36
43
  # :nocov:
37
- items.map { |item| [item.send(primary_key_name), item] }.to_h
44
+ items.to_h { |item| [item.send(primary_key_name), item] }
38
45
  # :nocov:
39
46
  end
40
47
 
41
- def match_items(items:, primary_keys:)
42
- items = items_with_primary_keys(items: items)
43
- found = []
44
- missing = []
45
-
46
- primary_keys.each do |key|
47
- item = items[key]
48
-
49
- item.nil? ? (missing << key) : (found << item)
50
- end
51
-
52
- [found, missing]
48
+ def not_found_error(primary_key_value)
49
+ Cuprum::Collections::Errors::NotFound.new(
50
+ attribute_name: primary_key_name,
51
+ attribute_value: primary_key_value,
52
+ collection_name: collection_name,
53
+ primary_key: true
54
+ )
53
55
  end
54
56
 
55
57
  def process(
@@ -58,16 +60,15 @@ module Cuprum::Collections::Commands
58
60
  envelope: false,
59
61
  scope: nil
60
62
  )
61
- query = apply_query(primary_keys: primary_keys, scope: scope)
62
- items = step do
63
- handle_missing_items(
64
- allow_partial: allow_partial,
65
- items: query.to_a,
66
- primary_keys: primary_keys
67
- )
68
- end
69
-
70
- envelope ? wrap_items(items) : items
63
+ query = apply_query(primary_keys: primary_keys, scope: scope)
64
+ items = items_with_primary_keys(items: query.to_a)
65
+ results = build_results(items: items, primary_keys: primary_keys)
66
+
67
+ build_result_list(
68
+ results,
69
+ allow_partial: allow_partial,
70
+ envelope: envelope
71
+ )
71
72
  end
72
73
 
73
74
  def wrap_items(items)
@@ -21,9 +21,10 @@ module Cuprum::Collections::Commands
21
21
  return if item
22
22
 
23
23
  error = Cuprum::Collections::Errors::NotFound.new(
24
- collection_name: collection_name,
25
- primary_key_name: primary_key_name,
26
- primary_key_values: [primary_key]
24
+ attribute_name: primary_key_name,
25
+ attribute_value: primary_key,
26
+ collection_name: collection_name,
27
+ primary_key: true
27
28
  )
28
29
  Cuprum::Result.new(error: error)
29
30
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/commands'
4
+
5
+ module Cuprum::Collections::Commands
6
+ # Command for building, validating and inserting an entity into a collection.
7
+ #
8
+ # @example Creating An Entity
9
+ # command =
10
+ # Cuprum::Collections::Commands::Create.new(collection:)
11
+ # .new(collection: books_collection)
12
+ #
13
+ # # With Invalid Attributes
14
+ # attributes = { 'title' => '' }
15
+ # result = command.call(attributes: attributes)
16
+ # result.success?
17
+ # #=> false
18
+ # result.error
19
+ # #=> an instance of Cuprum::Collections::Errors::FailedValidation
20
+ # books_collection.query.count
21
+ # #=> 0
22
+ #
23
+ # # With Valid Attributes
24
+ # attributes = { 'title' => 'Gideon the Ninth' }
25
+ # result = command.call(attributes: attributes)
26
+ # result.success?
27
+ # #=> true
28
+ # result.value
29
+ # #=> a Book with title 'Gideon the Ninth'
30
+ # books_collection.query.count
31
+ # #=> 1
32
+ class Create < Cuprum::Command
33
+ # @param collection [Object] The collection used to store the entity.
34
+ # @param contract [Stannum::Constraint] The constraint used to validate the
35
+ # entity. If not given, defaults to the default contract for the
36
+ # collection.
37
+ def initialize(collection:, contract: nil)
38
+ super()
39
+
40
+ @collection = collection
41
+ @contract = contract
42
+ end
43
+
44
+ # @return [Object] the collection used to store the entity.
45
+ attr_reader :collection
46
+
47
+ # @return [Stannum::Constraint] the constraint used to validate the entity.
48
+ attr_reader :contract
49
+
50
+ private
51
+
52
+ def process(attributes:)
53
+ entity = step { collection.build_one.call(attributes: attributes) }
54
+
55
+ step { collection.validate_one.call(contract: contract, entity: entity) }
56
+
57
+ collection.insert_one.call(entity: entity)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/commands'
4
+ require 'cuprum/collections/errors/not_found'
5
+ require 'cuprum/collections/errors/not_unique'
6
+
7
+ module Cuprum::Collections::Commands
8
+ # Command for finding a unique entity by a query or set of attributes.
9
+ #
10
+ # @example Finding An Entity By Attributes
11
+ # command =
12
+ # Cuprum::Collections::Commands::FindOneMatching
13
+ # .new(collection: books_collection)
14
+ #
15
+ # # With an attributes Hash that matches one entity.
16
+ # result = command.call(attributes: { 'title' => 'Gideon the Ninth' })
17
+ # result.success?
18
+ # #=> true
19
+ # result.value
20
+ # #=> a Book with title 'Gideon the Ninth'
21
+ #
22
+ # # With an attributes Hash that matches multiple entities.
23
+ # result = command.call(attributes: { 'author' => 'Tamsyn Muir' })
24
+ # result.success?
25
+ # #=> false
26
+ # result.error
27
+ # #=> an instance of Cuprum::Collections::NotUnique
28
+ #
29
+ # # With an attributes Hash that does not match any entities.
30
+ # result = command.call(
31
+ # attributes: {
32
+ # 'author' => 'Becky Chambers',
33
+ # 'series' => 'The Locked Tomb'
34
+ # }
35
+ # )
36
+ # result.success?
37
+ # #=> false
38
+ # result.error
39
+ # #=> an instance of Cuprum::Collections::NotFound
40
+ #
41
+ # @example Finding An Entity By Query
42
+ # command =
43
+ # Cuprum::Collections::Commands::FindOneMatching
44
+ # .new(collection: collection)
45
+ #
46
+ # # With a query that matches one entity.
47
+ # result = command.call do
48
+ # {
49
+ # 'series' => 'The Lord of the Rings',
50
+ # 'published_at' => greater_than('1955-01-01')
51
+ # }
52
+ # end
53
+ # result.success?
54
+ # #=> true
55
+ # result.value
56
+ # #=> a Book matching the query
57
+ #
58
+ # # With a query that matches multiple entities.
59
+ # result = command.call do
60
+ # {
61
+ # 'series' => 'The Lord of the Rings',
62
+ # 'published_at' => less_than('1955-01-01')
63
+ # }
64
+ # end
65
+ # result.success?
66
+ # #=> false
67
+ # result.error
68
+ # #=> an instance of Cuprum::Collections::NotUnique
69
+ #
70
+ # # With an attributes Hash that does not match any entities.
71
+ # result = command.call do
72
+ # {
73
+ # 'series' => 'The Lord of the Rings',
74
+ # 'published_at' => less_than('1954-01-01')
75
+ # }
76
+ # end
77
+ # result.success?
78
+ # #=> false
79
+ # result.error
80
+ # #=> an instance of Cuprum::Collections::NotFound
81
+ class FindOneMatching < Cuprum::Command
82
+ # @param collection [#find_matching] The collection to query.
83
+ def initialize(collection:)
84
+ super()
85
+
86
+ @collection = collection
87
+ end
88
+
89
+ # @return [#find_matching] the collection to query.
90
+ attr_reader :collection
91
+
92
+ private
93
+
94
+ def error_params_for(attributes: nil, &block)
95
+ { collection_name: collection.collection_name }.merge(
96
+ if block_given?
97
+ { query: collection.query.where(&block) }
98
+ else
99
+ { attributes: attributes }
100
+ end
101
+ )
102
+ end
103
+
104
+ def not_found_error(attributes: nil, &block)
105
+ Cuprum::Collections::Errors::NotFound.new(
106
+ **error_params_for(attributes: attributes, &block)
107
+ )
108
+ end
109
+
110
+ def not_unique_error(attributes: nil, &block)
111
+ Cuprum::Collections::Errors::NotUnique.new(
112
+ **error_params_for(attributes: attributes, &block)
113
+ )
114
+ end
115
+
116
+ def process(attributes: nil, &block)
117
+ query = block || -> { attributes }
118
+ entities = step { collection.find_matching.call(limit: 2, &query) }
119
+
120
+ require_one_entity(attributes: attributes, entities: entities, &block)
121
+ end
122
+
123
+ def require_one_entity(attributes:, entities:, &block)
124
+ case entities.count
125
+ when 0
126
+ failure(not_found_error(attributes: attributes, &block))
127
+ when 1
128
+ entities.first
129
+ when 2
130
+ failure(not_unique_error(attributes: attributes, &block))
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/commands'
4
+
5
+ module Cuprum::Collections::Commands
6
+ # Command for assigning, validating and updating an entity in a collection.
7
+ #
8
+ # @example Updating An Entity
9
+ # command =
10
+ # Cuprum::Collections::Commands::Create.new(collection:)
11
+ # .new(collection: books_collection)
12
+ # entity =
13
+ # books_collection
14
+ # .find_matching { { 'title' => 'Gideon the Ninth' } }
15
+ # .value
16
+ # .first
17
+ #
18
+ # # With Invalid Attributes
19
+ # attributes = { 'author' => '' }
20
+ # result = command.call(attributes: attributes)
21
+ # result.success?
22
+ # #=> false
23
+ # result.error
24
+ # #=> an instance of Cuprum::Collections::Errors::FailedValidation
25
+ # books_collection
26
+ # .find_matching { { 'title' => 'Gideon the Ninth' } }
27
+ # .value
28
+ # .first['author']
29
+ # #=> 'Tamsyn Muir'
30
+ #
31
+ # # With Valid Attributes
32
+ # attributes = { 'series' => 'The Locked Tomb' }
33
+ # result = command.call(attributes: attributes)
34
+ # result.success?
35
+ # #=> true
36
+ # result.value
37
+ # #=> an instance of Book with title 'Gideon the Ninth' and series
38
+ # 'The Locked Tomb'
39
+ # books_collection
40
+ # .find_matching { { 'title' => 'Gideon the Ninth' } }
41
+ # .value
42
+ # .first['series']
43
+ # #=> 'The Locked Tomb'
44
+ class Update < Cuprum::Command
45
+ # @param collection [Object] The collection used to store the entity.
46
+ # @param contract [Stannum::Constraint] The constraint used to validate the
47
+ # entity. If not given, defaults to the default contract for the
48
+ # collection.
49
+ def initialize(collection:, contract: nil)
50
+ super()
51
+
52
+ @collection = collection
53
+ @contract = contract
54
+ end
55
+
56
+ # @return [Object] the collection used to store the entity.
57
+ attr_reader :collection
58
+
59
+ # @return [Stannum::Constraint] the constraint used to validate the entity.
60
+ attr_reader :contract
61
+
62
+ private
63
+
64
+ def process(attributes:, entity:)
65
+ entity = step do
66
+ collection.assign_one.call(attributes: attributes, entity: entity)
67
+ end
68
+
69
+ step { collection.validate_one.call(entity: entity, contract: contract) }
70
+
71
+ step { collection.update_one.call(entity: entity) }
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ require 'cuprum/collections/commands'
6
+ require 'cuprum/collections/commands/create'
7
+ require 'cuprum/collections/commands/find_one_matching'
8
+ require 'cuprum/collections/commands/update'
9
+
10
+ module Cuprum::Collections::Commands
11
+ # Command for creating or updating an entity from an attributes Hash.
12
+ #
13
+ # @example Creating Or Updating An Entity By Primary Key
14
+ # command =
15
+ # Cuprum::Collections::Commands::Upsert
16
+ # .new(collection: books_collection)
17
+ #
18
+ # # Creating A New Entity
19
+ # books_collection.query.count
20
+ # #=> 0
21
+ # attributes = {
22
+ # 'id' => 0
23
+ # 'title' => 'Gideon the Ninth',
24
+ # 'author' => 'Tamsyn Muir'
25
+ # }
26
+ # result = command.call(attributes: attributes)
27
+ # result.value
28
+ # #=> a Book with id 0, title 'Gideon the Ninth', and author 'Tamsyn Muir'
29
+ # books_collection.query.count
30
+ # #=> 1
31
+ #
32
+ # # Updating An Existing Entity
33
+ # attributes = {
34
+ # 'id' => 0
35
+ # 'series' => 'The Locked Tomb'
36
+ # }
37
+ # result = command.call(attributes: attributes)
38
+ # result.value
39
+ # #=> a Book with id 0, title 'Gideon the Ninth', author 'Tamsyn Muir', and
40
+ # series 'The Locked Tomb'
41
+ # books_collection.query.count
42
+ # #=> 1
43
+ #
44
+ # @example Creating Or Updating An Entity By Attributes
45
+ # command =
46
+ # Cuprum::Collections::Commands::Upsert
47
+ # .new(attribute_names: %w[title], collection: books_collection)
48
+ #
49
+ # # Creating A New Entity
50
+ # books_collection.query.count
51
+ # #=> 0
52
+ # attributes = {
53
+ # 'id' => 0
54
+ # 'title' => 'Gideon the Ninth',
55
+ # 'author' => 'Tamsyn Muir'
56
+ # }
57
+ # result = command.call(attributes: attributes)
58
+ # result.value
59
+ # #=> a Book with id 0, title 'Gideon the Ninth', and author 'Tamsyn Muir'
60
+ # books_collection.query.count
61
+ # #=> 1
62
+ #
63
+ # # Updating An Existing Entity
64
+ # attributes = {
65
+ # 'title' => 'Gideon the Ninth',
66
+ # 'series' => 'The Locked Tomb'
67
+ # }
68
+ # result = command.call(attributes: attributes)
69
+ # result.value
70
+ # #=> a Book with id 0, title 'Gideon the Ninth', author 'Tamsyn Muir', and
71
+ # series 'The Locked Tomb'
72
+ # books_collection.query.count
73
+ # #=> 1
74
+ class Upsert < Cuprum::Command
75
+ # @param attribute_names [String, Symbol, Array<String, Symbol>] The names
76
+ # of the attributes used to find the unique entity.
77
+ # @param collection [Object] The collection used to store the entity.
78
+ # @param contract [Stannum::Constraint] The constraint used to validate the
79
+ # entity. If not given, defaults to the default contract for the
80
+ # collection.
81
+ def initialize(collection:, attribute_names: 'id', contract: nil)
82
+ super()
83
+
84
+ @attribute_names = normalize_attribute_names(attribute_names)
85
+ @collection = collection
86
+ @contract = contract
87
+ end
88
+
89
+ # @return [Array<String>] the names of the attributes used to find the
90
+ # unique entity.
91
+ attr_reader :attribute_names
92
+
93
+ # @return [Object] the collection used to store the entity.
94
+ attr_reader :collection
95
+
96
+ # @return [Stannum::Constraint] the constraint used to validate the entity.
97
+ attr_reader :contract
98
+
99
+ private
100
+
101
+ def create_entity(attributes:)
102
+ Cuprum::Collections::Commands::Create
103
+ .new(collection: collection, contract: contract)
104
+ .call(attributes: attributes)
105
+ end
106
+
107
+ def filter_attributes(attributes:)
108
+ tools
109
+ .hash_tools
110
+ .convert_keys_to_strings(attributes)
111
+ .select { |key, _| attribute_names.include?(key) }
112
+ end
113
+
114
+ def find_entity(attributes:)
115
+ filtered = filter_attributes(attributes: attributes)
116
+ result =
117
+ Cuprum::Collections::Commands::FindOneMatching
118
+ .new(collection: collection)
119
+ .call(attributes: filtered)
120
+
121
+ return if result.error.is_a?(Cuprum::Collections::Errors::NotFound)
122
+
123
+ result
124
+ end
125
+
126
+ def normalize_attribute_names(attribute_names)
127
+ names = Array(attribute_names)
128
+
129
+ raise ArgumentError, "attribute names can't be blank" if names.empty?
130
+
131
+ names = names.map do |name|
132
+ unless name.is_a?(String) || name.is_a?(Symbol)
133
+ raise ArgumentError, "invalid attribute name #{name.inspect}"
134
+ end
135
+
136
+ name.to_s
137
+ end
138
+
139
+ Set.new(names)
140
+ end
141
+
142
+ def process(attributes:)
143
+ entity = step { find_entity(attributes: attributes) }
144
+
145
+ if entity
146
+ update_entity(attributes: attributes, entity: entity)
147
+ else
148
+ create_entity(attributes: attributes)
149
+ end
150
+ end
151
+
152
+ def tools
153
+ SleepingKingStudios::Tools::Toolbelt.instance
154
+ end
155
+
156
+ def update_entity(attributes:, entity:)
157
+ Cuprum::Collections::Commands::Update
158
+ .new(collection: collection, contract: contract)
159
+ .call(attributes: attributes, entity: entity)
160
+ end
161
+ end
162
+ end
@@ -3,6 +3,11 @@
3
3
  require 'cuprum/collections'
4
4
 
5
5
  module Cuprum::Collections
6
- # Namespace for abstract commands implementing collection functionality.
7
- module Commands; end
6
+ # Namespace for abstract commands and collection-independent commands.
7
+ module Commands
8
+ autoload :Create, 'cuprum/collections/commands/create'
9
+ autoload :FindOneMatching, 'cuprum/collections/commands/find_one_matching'
10
+ autoload :Update, 'cuprum/collections/commands/update'
11
+ autoload :Upsert, 'cuprum/collections/commands/upsert'
12
+ end
8
13
  end