cuprum-collections 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/README.md +255 -8
  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/commands/abstract_find_many.rb +33 -32
  11. data/lib/cuprum/collections/commands/abstract_find_one.rb +4 -3
  12. data/lib/cuprum/collections/commands/create.rb +60 -0
  13. data/lib/cuprum/collections/commands/find_one_matching.rb +134 -0
  14. data/lib/cuprum/collections/commands/update.rb +74 -0
  15. data/lib/cuprum/collections/commands/upsert.rb +162 -0
  16. data/lib/cuprum/collections/commands.rb +7 -2
  17. data/lib/cuprum/collections/errors/abstract_find_error.rb +210 -0
  18. data/lib/cuprum/collections/errors/already_exists.rb +4 -72
  19. data/lib/cuprum/collections/errors/extra_attributes.rb +8 -18
  20. data/lib/cuprum/collections/errors/failed_validation.rb +5 -18
  21. data/lib/cuprum/collections/errors/invalid_parameters.rb +7 -15
  22. data/lib/cuprum/collections/errors/invalid_query.rb +5 -15
  23. data/lib/cuprum/collections/errors/missing_default_contract.rb +5 -17
  24. data/lib/cuprum/collections/errors/not_found.rb +4 -67
  25. data/lib/cuprum/collections/errors/not_unique.rb +18 -0
  26. data/lib/cuprum/collections/errors/unknown_operator.rb +7 -17
  27. data/lib/cuprum/collections/errors.rb +13 -1
  28. data/lib/cuprum/collections/queries/ordering.rb +2 -2
  29. data/lib/cuprum/collections/repository.rb +23 -10
  30. data/lib/cuprum/collections/rspec/collection_contract.rb +140 -103
  31. data/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +8 -6
  32. data/lib/cuprum/collections/rspec/find_many_command_contract.rb +114 -34
  33. data/lib/cuprum/collections/rspec/find_one_command_contract.rb +12 -9
  34. data/lib/cuprum/collections/rspec/insert_one_command_contract.rb +4 -3
  35. data/lib/cuprum/collections/rspec/query_contract.rb +3 -3
  36. data/lib/cuprum/collections/rspec/querying_contract.rb +2 -2
  37. data/lib/cuprum/collections/rspec/repository_contract.rb +167 -93
  38. data/lib/cuprum/collections/rspec/update_one_command_contract.rb +4 -3
  39. data/lib/cuprum/collections/version.rb +1 -1
  40. data/lib/cuprum/collections.rb +1 -0
  41. metadata +20 -89
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78622fdeb8107cc8df27120e104a7b4ab1239569f8677b648b3462b36b4c008c
4
- data.tar.gz: 0ca7eeb33a33d477888851d1a515b9e0c7cae3dc80a7a85f5455747ea1275ddc
3
+ metadata.gz: dda6613af3def3463515616a0b448dce3ce0fb666b6b81b9c9d54cf9e4f6bef0
4
+ data.tar.gz: e8cae0cd8e96daf887268b0b467352d34a7a93357729c14115375f7d14d2e2d8
5
5
  SHA512:
6
- metadata.gz: 40a8281d4232bf7e8e78450d99dde47089dbbc039c7f7c689c579d98bf22d3069e3e9c0f3066ebca1d4ce292fb170738f5ed71afb8b49a875648333d81523f08
7
- data.tar.gz: fc9c9d23db953377b34397a89fdd4098e170b1b2db3f84261562c473d932e881dbcc92237d5fb53a8b819a722bef5d89f545418fca9e527e8dfc0271c4aacd3c
6
+ metadata.gz: 138cf1bd095d6d03e196c90ac4779ee6d8dec8743f8cb321c4114117c707ef0124f378642653beef0e3db1c0d127cebd05967a4356ce5361cd3294bd298642e7
7
+ data.tar.gz: 968b185b8e1b0447b55acc093387ad527e0ba5154b0e4a24d2ace52cf258ca29939eefa0cc772451703d1b56286dce907c84402a735a360152d3573ea8cb5db7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Collections
6
+
7
+ Updated `Cuprum::Collections::Basic::Collection`.
8
+
9
+ - Implemented `#count` method.
10
+ - Implemented `#qualified_name` method.
11
+
12
+ ### Commands
13
+
14
+ Implemented built-in Commands, which take a `:collection` parameter:
15
+
16
+ - `Commands::Create`
17
+ - `Commands::FindOneMatching`
18
+ - `Commands::Update`
19
+ - `Commands::Upsert`
20
+
3
21
  ## 0.2.0
4
22
 
5
23
  Implemented `Cuprum::Collections::Repository`.
data/README.md CHANGED
@@ -26,7 +26,7 @@ The Ruby ecosystem has a wide variety of tools and libraries for managing data a
26
26
 
27
27
  ### Compatibility
28
28
 
29
- Cuprum::Collections is tested against Ruby (MRI) 2.6 through 2.7.
29
+ Cuprum::Collections is tested against Ruby (MRI) 2.7 through 3.2.
30
30
 
31
31
  ### Documentation
32
32
 
@@ -34,9 +34,9 @@ Documentation is generated using [YARD](https://yardoc.org/), and can be generat
34
34
 
35
35
  ### License
36
36
 
37
- Copyright (c) 2020-2021 Rob Smith
37
+ Copyright (c) 2020-2023 Rob Smith
38
38
 
39
- Stannum is released under the [MIT License](https://opensource.org/licenses/MIT).
39
+ Cuprum::Collections is released under the [MIT License](https://opensource.org/licenses/MIT).
40
40
 
41
41
  ### Contribute
42
42
 
@@ -105,6 +105,13 @@ end
105
105
 
106
106
  Because a collection can represent any sort of data, from a raw Ruby Hash to an ORM record, the term used to indicate "one item in the collection" is an *entity*. Likewise, the class of the items in the collection is the *entity_class*. In our example above, our entities are books, and the entity class is Hash.
107
107
 
108
+ Each collection also defines the following methods:
109
+
110
+ - `#collection_name`: The collection name is a short description of what the collection contains. For example, a collection of `Book` objects might have a collection name of `'books'`, while a collection of `Authorization::Credentials::ApiKey` objects might have a collection name of `'api_keys'`.
111
+ - `#qualified_name`: The qualified name is a full description of the collection, and should be unique. For example, a collection of `Book` objects might have a qualified name of `'books'`, while a collection of `Authorization::Credentials::ApiKey` objects might have a qualified name of `'authorization/credentials/api_keys'`.
112
+
113
+ As a general rule, the `#collection_name` is used when displaying information to the user, while the `#qualified_name` is used to uniquely identify the collection (such as when adding to or retrieving from a [Repository](#repositories)).
114
+
108
115
  <a id="commands"></a>
109
116
 
110
117
  #### Commands
@@ -214,7 +221,7 @@ If the collection does not include an entity with each of the specified primary
214
221
 
215
222
  ##### Find Matching
216
223
 
217
- The `FindMatching` command takes a set of query parameters and queries data from the collection. You can specify filters using the `:where` keyword or by passing a block, sort the results using the `:order` keyword, or return a subset of the results using the `:limit` and `:offset` keywords. For full details on performing queries, see [Queries](#queries), below.
224
+ The `FindMatching` command takes a set of query parameters and queries data from the collection. You can specify filters using the `:where` keyword or by passing a block, sort the results using the `:order` keyword, or return a subset of the results using the `:limit` and `:offset` keywords.
218
225
 
219
226
  ```ruby
220
227
  result =
@@ -260,7 +267,11 @@ The `FindMatching` command has several options:
260
267
  #=> { books: [{ ... }, { ... }, { ... }] }
261
268
  ```
262
269
 
270
+ - The `:limit` keyword caps the number of results returned.
271
+ - The `:offset` keyword skips the specified number of results.
272
+ - The `:order` keyword specifies the order of results.
263
273
  - The `:scope` keyword allows you to pass a query to the command. Only entities that match the given scope will be found and returned by `#find_matching`.
274
+ - The `:where` keyword defines filters for which results are to be returned.
264
275
 
265
276
  ##### Find One
266
277
 
@@ -299,7 +310,7 @@ The `InsertOne` command takes an entity and inserts that entity into the collect
299
310
 
300
311
  ```ruby
301
312
  book = { 'id' => 10, 'title' => 'Gideon the Ninth', 'author' => 'Tamsyn Muir' }
302
- result = collection.insert_one.call(entity: entity)
313
+ result = collection.insert_one.call(entity: book)
303
314
 
304
315
  result.value
305
316
  #=> {
@@ -369,16 +380,16 @@ If the collection does not specify a default contract and no `:contract` keyword
369
380
  require 'cuprum/collections/repository'
370
381
  ```
371
382
 
372
- A repository is a group of collections. While a collection might be be a single data set, such as the records in a table, the repository represents all of the data sets in a data source, such as the tables in a database.
383
+ A repository is a group of collections. While a collection might be be a single data set, such as the records in a table, the repository represents all of the data sets in a data source, such as the tables in a database. Each repository uses the `#qualified_name` of its collections as a unique key.
373
384
 
374
385
  ```ruby
375
386
  repository = Cuprum::Collections::Repository.new
376
- repository.key?('book')
387
+ repository.key?('books')
377
388
  #=> false
378
389
 
379
390
  repository.add(books_collection)
380
391
 
381
- repository.key?('book')
392
+ repository.key?('books')
382
393
  #=> true
383
394
  repository.keys
384
395
  #=> ['books']
@@ -386,6 +397,31 @@ repository['books']
386
397
  #=> the books collection
387
398
  ```
388
399
 
400
+ When accessing a collection with a qualified name, you must pass the qualified name to `#[]` or `#key?`, rather than the collection name.
401
+
402
+ ```ruby
403
+ repository = Cuprum::Collections::Repository.new
404
+ repository.key?('api_keys')
405
+ #=> false
406
+ repository.key?('authorization/credentials/api_keys')
407
+ #=> false
408
+
409
+ api_keys_collection.collection_name
410
+ #=> 'api_keys'
411
+ api_keys_collection.qualified_name
412
+ #=> 'authorization/credentials/api_keys'
413
+ repository.add(api_keys_collection)
414
+
415
+ repository.key?('api_keys')
416
+ #=> false
417
+ repository.key?('authorization/credentials/api_keys')
418
+ #=> true
419
+ repository.keys
420
+ #=> ['authorization/credentials/api_keys']
421
+ repository['authorization/credentials/api_keys']
422
+ #=> the api keys collection
423
+ ```
424
+
389
425
  #### Basic Collection
390
426
 
391
427
  ```ruby
@@ -412,6 +448,7 @@ You can also specify some optional keywords:
412
448
  - The `:member_name` parameter is used to create an envelope for singular query commands such as the `FindOne` command. If not given, the member name will be generated automatically as a singular form of the collection name.
413
449
  - The `:primary_key_name` parameter specifies the attribute that serves as the primary key for the collection entities. The default value is `:id`.
414
450
  - The `:primary_key_type` parameter specifies the type of the primary key attribute. The default value is `Integer`.
451
+ - The `:qualified_name` parameter sets the qualified name for the collection. It is used to uniquely identify the collection in a repository.
415
452
 
416
453
  ##### Basic Repositories
417
454
 
@@ -1007,3 +1044,213 @@ query.each.map(&:title)
1007
1044
  # 'The Farthest Shore'
1008
1045
  # ]
1009
1046
  ```
1047
+
1048
+ <a id="builtin-commands"></a>
1049
+
1050
+ ### Built In Commands
1051
+
1052
+ `Cuprum::Collections` defines some basic commands. Each command takes a `:collection` parameter, which is used for performing the data operations.
1053
+
1054
+ #### Create
1055
+
1056
+ ```ruby
1057
+ require 'cuprum/collections/commands/create'
1058
+ ```
1059
+
1060
+ The `Create` command takes an attributes Hash and adds an entity with those attributes to the collection. Internally, it calls the `#build_one`, `#validate_one`, and `#insert_one` commands on the collection.
1061
+
1062
+ ```ruby
1063
+ command = Cuprum::Collections::Commands::Create.new(collection: books_collection)
1064
+
1065
+ books_collection.count
1066
+ #=> 0
1067
+ result = command.call(attributes: { 'title' => 'Gideon the Ninth' })
1068
+ result.value
1069
+ #=> a Book with title "Gideon the Ninth"
1070
+ books_collection.count
1071
+ #=> 1
1072
+
1073
+ books_collection.find_matching.call { { 'title' => 'Gideon the Ninth' } }.value.first
1074
+ #=> a Book with title "Gideon the Ninth"
1075
+ ```
1076
+
1077
+ If the contract does not match the entity, the `Create` command will return a failing result with a `ValidationFailed` error.
1078
+
1079
+ If the collection does not specify a default contract and no :contract keyword is provided, the `Create` command will return a failing result with a `MissingDefaultContract` error.
1080
+
1081
+ If the collection already includes an entity with the specified primary key, the `Create` command will return a failing result with an `AlreadyExists` error.
1082
+
1083
+ #### FindOneMatching
1084
+
1085
+ ```ruby
1086
+ require 'cuprum/collections/commands/find_one_matching'
1087
+ ```
1088
+
1089
+ The `FindOneMatching` command takes either an attributes hash or a [Query](#queries) block, and returns the unique entity from the collection with those entities or matching that query.
1090
+
1091
+ ```ruby
1092
+ command = Cuprum::Collections::Commands::FindOneMatching.new(collection: books_collection)
1093
+
1094
+ result = command.call(attributes: { 'title' => 'Gideon the Ninth'})
1095
+ result.success?
1096
+ #=> true
1097
+ result.value
1098
+ #=> the unique Book with title "Gideon the Ninth"
1099
+
1100
+ result = command.call do
1101
+ {
1102
+ 'series' => 'The Locked Tomb',
1103
+ 'published_at' => less_than('2020-01-01')
1104
+ }
1105
+ end
1106
+ result.success?
1107
+ #=> true
1108
+ result.value
1109
+ #=> the unique Book with the given series and published before the given date
1110
+ ```
1111
+
1112
+ If there are no entities in the collection matching the attributes or query, the `FindOneMatching` command returns a failing result with a `Cuprum::Collections::Errors::NotFound` error.
1113
+
1114
+ ```ruby
1115
+ result = command.call(attributes: { 'title' => 'Gideon the Eleventh'})
1116
+ result.success?
1117
+ #=> false
1118
+ result.error
1119
+ #=> an instance of Cuprum::Collections::Errors::NotFound
1120
+ ```
1121
+
1122
+ If there are two or more entities in the collection matching the attributes or query, the `FindOneMatching` command returns a failing result with a `Cuprum::Collections::Errors::NotUnique` error.
1123
+
1124
+ ```ruby
1125
+ result = command.call(attributes: { 'author' => 'Tamsyn Muir'})
1126
+ result.success?
1127
+ #=> false
1128
+ result.error
1129
+ #=> an instance of Cuprum::Collections::Errors::NotUnique
1130
+ ```
1131
+
1132
+ #### Update
1133
+
1134
+ ```ruby
1135
+ require 'cuprum/collections/commands/update'
1136
+ ```
1137
+
1138
+ The `Update` command takes an attributes Hash and an entity and updates the corresponding entity in the collection with those attributes. Internally, it calls the `#assign_one`, `#validate_one`, and `#update_one` commands on the collection.
1139
+
1140
+ ```ruby
1141
+ command = Cuprum::Collections::Commands::Update.new(collection: books_collection)
1142
+
1143
+ entity = books_collection.find_matching.call { { 'title' => 'Gideon the Ninth' } }.value.first
1144
+ #=> a Book with title "Gideon the Ninth" and series "The Locked Tomb"
1145
+
1146
+ result = command.call(
1147
+ attributes: { 'series' => 'Space Necromancers' },
1148
+ entity: entity
1149
+ )
1150
+ result.value
1151
+ #=> a Book with title "Gideon the Ninth" and series "Space Necromancers"
1152
+
1153
+ books_collection.find_matching.call { { 'title' => 'Gideon the Ninth' } }.value.first
1154
+ #=> a Book with title "Gideon the Ninth" and series "Space Necromancers"
1155
+ ```
1156
+
1157
+ If the contract does not match the entity, the `Update` command will return a failing result with a `ValidationFailed` error.
1158
+
1159
+ If the collection does not specify a default contract and no :contract keyword is provided, the `Update` command will return a failing result with a `MissingDefaultContract` error.
1160
+
1161
+ If the collection does not include an entity with the specified entity's primary key, the `Update` command will return a failing result with a `NotFound` error.
1162
+
1163
+ #### Upsert
1164
+
1165
+ ```ruby
1166
+ require 'cuprum/collections/commands/upsert'
1167
+ ```
1168
+
1169
+ The `Upsert` command takes an attributes Hash and checks the collection for an entity with a matching primary key. If an entity is found, it updates the entity with the given attributes, as per the `Update` command (see above); if an entity is not found, it creates a new entity with the given attributes, as per the `Create` command.
1170
+
1171
+ ```ruby
1172
+ command = Cuprum::Collections::Commands::Upsert.new(collection: books_collection)
1173
+
1174
+ # Creating An Entity
1175
+ books_collection.count
1176
+ #=> 0
1177
+ result = command.call(attributes: { 'id' => 0, 'title' => 'Gideon the Ninth' })
1178
+ result.value
1179
+ #=> a Book with id 0 and title "Gideon the Ninth"
1180
+ books_collection.count
1181
+ #=> 1
1182
+
1183
+ books_collection.find_matching.call { { 'title' => 'Gideon the Ninth' } }.value.first
1184
+ #=> a Book with id 0 and title "Gideon the Ninth"
1185
+
1186
+ # Updating An Entity
1187
+ result = command.call(attributes: { 'id' => 0, 'author' => 'Tamsyn Muir' })
1188
+ result.value
1189
+ #=> a Book with id 0, title "Gideon the Ninth", and author "Tamsyn Muir"
1190
+
1191
+ books_collection.find_matching.call { { 'title' => 'Gideon the Ninth' } }.value.first
1192
+ #=> a Book with id 0, title "Gideon the Ninth", and author "Tamsyn Muir"
1193
+ ```
1194
+
1195
+ The `Upsert` command can also be configured with an attribute name or list of attribute names; the command will then search for an entity in the collection matching those attributes, rather than by primary key.
1196
+
1197
+ ```ruby
1198
+ command =
1199
+ Cuprum::Collections::Commands::Upsert
1200
+ .new(attribute_names: %w[title author], collection: books_collection)
1201
+ other_entity = books_collection.build_one.call(
1202
+ attributes: {
1203
+ 'id' => 0,
1204
+ 'title' => 'Gideon the Ninth',
1205
+ 'author' => 'T. M.'
1206
+ }
1207
+ )
1208
+ books_collection.insert_one.call(entity: entity)
1209
+
1210
+ # Creating An Entity
1211
+ books_collection.count
1212
+ #=> 1
1213
+ result = command.call(
1214
+ attributes: {
1215
+ 'id' => 1,
1216
+ 'title' => 'Gideon the Ninth',
1217
+ 'author' => 'Tamsyn Muir'
1218
+ }
1219
+ )
1220
+ result.value
1221
+ #=> a Book with id 1, title "Gideon the Ninth", and author "Tamsyn Muir"
1222
+ books_collection.count
1223
+ #=> 1
1224
+
1225
+ books_collection.find_matching.call { { 'title' => 'Gideon the Ninth' } }.value.count
1226
+ #=> 2
1227
+ books_collection.find_matching.call { { 'title' => 'Gideon the Ninth' } }.value.map(&:author)
1228
+ #=> ['T. M.', 'Tamsyn Muir']
1229
+
1230
+ # Updating An Entity
1231
+ result = command.call(
1232
+ attributes: {
1233
+ 'title' => 'Gideon the Ninth',
1234
+ 'author' => 'Tamsyn Muir',
1235
+ 'series' => 'The Locked Tomb'
1236
+ }
1237
+ )
1238
+ result.value
1239
+ #=> a Book with id 1, title "Gideon the Ninth", author "Tamsyn Muir", and series
1240
+ # "The Locked Tomb"
1241
+
1242
+ books_collection.find_matching.call do
1243
+ {
1244
+ 'title' => 'Gideon the Ninth',
1245
+ 'author' => 'Tamsyn Muir'
1246
+ }
1247
+ end.value.first
1248
+ #=> a Book with id 1, title "Gideon the Ninth", author "Tamsyn Muir", and series
1249
+ # "The Locked Tomb"
1250
+ ```
1251
+
1252
+ If there are two or more entities in the collection matching the attributes or query, the `Upsert` command returns a failing result with a `Cuprum::Collections::Errors::NotUnique` error.
1253
+
1254
+ If the contract does not match the entity, the `Upsert` command will return a failing result with a `ValidationFailed` error.
1255
+
1256
+ If the collection does not specify a default contract and no :contract keyword is provided, the `Upsert` command will return a failing result with a `MissingDefaultContract` error.
@@ -17,6 +17,8 @@ module Cuprum::Collections::Basic
17
17
  # Defaults to :id.
18
18
  # @param primary_key_type [Class, Stannum::Constraint] The type of the
19
19
  # primary key attribute. Defaults to Integer.
20
+ # @param qualified_name [String] The qualified name of the collection, which
21
+ # should be unique. Defaults to the collection name.
20
22
  # @param options [Hash<Symbol>] Additional options for the command.
21
23
  def initialize( # rubocop:disable Metrics/ParameterLists
22
24
  collection_name:,
@@ -25,6 +27,7 @@ module Cuprum::Collections::Basic
25
27
  member_name: nil,
26
28
  primary_key_name: :id,
27
29
  primary_key_type: Integer,
30
+ qualified_name: nil,
28
31
  **options
29
32
  )
30
33
  super()
@@ -37,6 +40,7 @@ module Cuprum::Collections::Basic
37
40
  @options = options
38
41
  @primary_key_name = primary_key_name
39
42
  @primary_key_type = primary_key_type
43
+ @qualified_name = qualified_name || @collection_name
40
44
  end
41
45
 
42
46
  # @return [String] the name of the collection.
@@ -62,6 +66,9 @@ module Cuprum::Collections::Basic
62
66
  # attribute.
63
67
  attr_reader :primary_key_type
64
68
 
69
+ # @return [String] the qualified name of the collection.
70
+ attr_reader :qualified_name
71
+
65
72
  command_class :assign_one do
66
73
  Cuprum::Collections::Basic::Commands::AssignOne
67
74
  .subclass(**command_options)
@@ -107,6 +114,12 @@ module Cuprum::Collections::Basic
107
114
  .subclass(**command_options)
108
115
  end
109
116
 
117
+ # @return [Integer] the count of items in the collection.
118
+ def count
119
+ query.count
120
+ end
121
+ alias size count
122
+
110
123
  # A new Query instance, used for querying against the collection data.
111
124
  #
112
125
  # @return [Cuprum::Collections::Basic::Query] the query.
@@ -28,9 +28,10 @@ module Cuprum::Collections::Basic::Commands
28
28
  return if index
29
29
 
30
30
  error = Cuprum::Collections::Errors::NotFound.new(
31
- collection_name: collection_name,
32
- primary_key_name: primary_key_name,
33
- primary_key_values: [primary_key]
31
+ attribute_name: primary_key_name,
32
+ attribute_value: primary_key,
33
+ collection_name: collection_name,
34
+ primary_key: true
34
35
  )
35
36
  Cuprum::Result.new(error: error)
36
37
  end
@@ -47,7 +47,7 @@ module Cuprum::Collections::Basic::Commands
47
47
 
48
48
  def items_with_primary_keys(items:)
49
49
  # :nocov:
50
- items.map { |item| [item[primary_key_name.to_s], item] }.to_h
50
+ items.to_h { |item| [item[primary_key_name.to_s], item] }
51
51
  # :nocov:
52
52
  end
53
53
 
@@ -32,9 +32,10 @@ module Cuprum::Collections::Basic::Commands
32
32
  return if index.nil?
33
33
 
34
34
  error = Cuprum::Collections::Errors::AlreadyExists.new(
35
- collection_name: collection_name,
36
- primary_key_name: primary_key_name,
37
- primary_key_values: value
35
+ attribute_name: primary_key_name,
36
+ attribute_value: value,
37
+ collection_name: collection_name,
38
+ primary_key: true
38
39
  )
39
40
  failure(error)
40
41
  end
@@ -32,9 +32,10 @@ module Cuprum::Collections::Basic::Commands
32
32
  return index unless index.nil?
33
33
 
34
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]
35
+ attribute_name: primary_key_name,
36
+ attribute_value: entity[primary_key_name.to_s],
37
+ collection_name: collection_name,
38
+ primary_key: true
38
39
  )
39
40
  failure(error)
40
41
  end
@@ -151,9 +151,9 @@ module Cuprum::Collections::Basic
151
151
  def filtered_data
152
152
  @filtered_data ||=
153
153
  data
154
- .yield_self { |ary| apply_filters(ary) }
155
- .yield_self { |ary| apply_order(ary) }
156
- .yield_self { |ary| apply_limit_offset(ary) }
154
+ .then { |ary| apply_filters(ary) }
155
+ .then { |ary| apply_order(ary) }
156
+ .then { |ary| apply_limit_offset(ary) }
157
157
  .map(&:dup)
158
158
  end
159
159
  end
@@ -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