cuprum-collections 0.1.0 → 0.3.0.rc.0

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