cuprum-collections 0.2.0 → 0.3.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 (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