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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53ab28a09a25a4de8a7ca405a3f59b45b5b9cec3fc0acd243a8815bfdfee80ae
4
- data.tar.gz: aef7d1257fd0997c284a1cad91118086ae2672789fe40d9648fc6a553fb70abd
3
+ metadata.gz: 077ded1de1d41e29689ed46422c0ba4b367d333a495f568f461c9a5a0d8c5278
4
+ data.tar.gz: 82e9ec27fcdf2975f37f24f680cd2a0d653ec529c6884a6e9c8ad8cd33a5f4ed
5
5
  SHA512:
6
- metadata.gz: 2f9544cd606500cca431300aa4432ab58a475fbb2ee6c813bbdef36d8a10a5cec3eea7b219eb16dfcf4a810fa022154e554e61373ad47e080818933f97f76d74
7
- data.tar.gz: 46f80cf9f3d575e564a46230fc65c984d4886f7f23ca62cdd2454f528a9d2f96fc70e31d23f222c7acd944ab5f5a9a468f94ed3a928a191d5bdaf0057219288a
6
+ metadata.gz: 623403206ee2a4ed953069e67e04f6c06038157fa3ac2701e65cd3e2739b49e4579a1379f2f04b7324df646de1b2c667749c8a9ae5c576bdbc587f88bcd7be7c
7
+ data.tar.gz: 5cc578f36e263ed5b83c46e902222286f47a1d2588aad8b43817686cd814242205c81d02591921895510ab551a8afdd96384c1a7fe7e0a4557dd991b5a641d0e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ Implemented `Cuprum::Collections::Repository`.
6
+
7
+ ### Collections
8
+
9
+ Updated `Cuprum::Collections::Basic::Collection`.
10
+
11
+ - Implemented `#count` method.
12
+ - Implemented `#qualified_name` method.
13
+
14
+ Implemented `Cuprum::Collections::Basic::Repository`.
15
+
16
+ ### Commands
17
+
18
+ Implemented built-in Commands, which take a `:collection` parameter:
19
+
20
+ - `Commands::Create`
21
+ - `Commands::FindOneMatching`
22
+ - `Commands::Update`
23
+ - `Commands::Upsert`
24
+
25
+ ### Queries
26
+
27
+ Fixed passing an attributes array as a query ordering.
28
+
3
29
  ## 0.1.0
4
30
 
5
31
  Initial version.
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
 
@@ -77,7 +77,7 @@ steps do
77
77
  # Build the book from attributes.
78
78
  book = step do
79
79
  collection.build_one.call(
80
- attributes: { id: 10, title: 'Gideon the Ninth', author: 'Tammsyn Muir' }
80
+ attributes: { id: 10, title: 'Gideon the Ninth', author: 'Tamsyn Muir' }
81
81
  )
82
82
  end
83
83
 
@@ -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
@@ -116,7 +123,7 @@ Structurally, a collection is a set of commands, which are instances of `Cuprum:
116
123
  The `AssignOne` command takes an attributes hash and an entity, and returns an instance of the entity class whose attributes are equal to the attributes hash merged into original entities attributes. Depending on the collection, `#assign_one` may or may not modify or return the original entity.
117
124
 
118
125
  ```ruby
119
- book = { 'id' => 10, 'title' => 'Gideon the Ninth', 'author' => 'Tammsyn Muir' }
126
+ book = { 'id' => 10, 'title' => 'Gideon the Ninth', 'author' => 'Tamsyn Muir' }
120
127
  attributes = { 'title' => 'Harrow the Ninth', 'published_at' => '2020-08-04' }
121
128
  result = collection.assign_one.call(attributes: attributes, entity: entity)
122
129
 
@@ -124,7 +131,7 @@ result.value
124
131
  #=> {
125
132
  # 'id' => 10,
126
133
  # 'title' => 'Harrow the Ninth',
127
- # 'author' => 'Tammsyn Muir',
134
+ # 'author' => 'Tamsyn Muir',
128
135
  # 'published_at' => '2020-08-04'
129
136
  # }
130
137
  ```
@@ -136,14 +143,14 @@ If the entity class specifies a set of attributes (such as the defined columns i
136
143
  The `BuildOne` command takes an attributes hash and returns a new instance of the entity class whose attributes are equal to the given attributes. This does not validate or persist the entity; it is equivalent to calling `entity_class.new` with the attributes.
137
144
 
138
145
  ```ruby
139
- attributes = { 'id' => 10, 'title' => 'Gideon the Ninth', 'author' => 'Tammsyn Muir' }
146
+ attributes = { 'id' => 10, 'title' => 'Gideon the Ninth', 'author' => 'Tamsyn Muir' }
140
147
  result = collection.build_one.call(attributes: attributes, entity: entity)
141
148
 
142
149
  result.value
143
150
  #=> {
144
151
  # 'id' => 10,
145
152
  # 'title' => 'Gideon the Ninth',
146
- # 'author' => 'Tammsyn Muir'
153
+ # 'author' => 'Tamsyn Muir'
147
154
  # }
148
155
  ```
149
156
 
@@ -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
 
@@ -298,14 +309,14 @@ If the collection does not include an entity with the specified primary key, the
298
309
  The `InsertOne` command takes an entity and inserts that entity into the collection.
299
310
 
300
311
  ```ruby
301
- book = { 'id' => 10, 'title' => 'Gideon the Ninth', 'author' => 'Tammsyn Muir' }
302
- result = collection.insert_one.call(entity: entity)
312
+ book = { 'id' => 10, 'title' => 'Gideon the Ninth', 'author' => 'Tamsyn Muir' }
313
+ result = collection.insert_one.call(entity: book)
303
314
 
304
315
  result.value
305
316
  #=> {
306
317
  # 'id' => 10,
307
318
  # 'title' => 'Gideon the Ninth',
308
- # 'author' => 'Tammsyn Muir'
319
+ # 'author' => 'Tamsyn Muir'
309
320
  # }
310
321
 
311
322
  collection.query.where(id: 10).exists?
@@ -351,7 +362,7 @@ contract = Stannum::Contract.new do
351
362
  property :title, Stannum::Constraints::Presence.new
352
363
  end
353
364
 
354
- book = { 'id' => 10, 'title' => 'Gideon the Ninth', 'author' => 'Tammsyn Muir' }
365
+ book = { 'id' => 10, 'title' => 'Gideon the Ninth', 'author' => 'Tamsyn Muir' }
355
366
  result = collection.validate_one.call(contract: contract, entity: book)
356
367
  result.success?
357
368
  #=> true
@@ -361,9 +372,59 @@ If the contract does not match the entity, the `#validate_one` command will retu
361
372
 
362
373
  If the collection does not specify a default contract and no `:contract` keyword is provided, the `#validate_one` command will return a failing result with a `MissingDefaultContract` error.
363
374
 
364
- #### Basic Collection
375
+ <a id="repositories"></a>
376
+
377
+ #### Repositories
378
+
379
+ ```ruby
380
+ require 'cuprum/collections/repository'
381
+ ```
382
+
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.
384
+
385
+ ```ruby
386
+ repository = Cuprum::Collections::Repository.new
387
+ repository.key?('books')
388
+ #=> false
389
+
390
+ repository.add(books_collection)
391
+
392
+ repository.key?('books')
393
+ #=> true
394
+ repository.keys
395
+ #=> ['books']
396
+ repository['books']
397
+ #=> the books collection
398
+ ```
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
365
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
366
423
  ```
424
+
425
+ #### Basic Collection
426
+
427
+ ```ruby
367
428
  require 'cuprum/collections/basic'
368
429
  ```
369
430
 
@@ -387,6 +448,41 @@ You can also specify some optional keywords:
387
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.
388
449
  - The `:primary_key_name` parameter specifies the attribute that serves as the primary key for the collection entities. The default value is `:id`.
389
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.
452
+
453
+ ##### Basic Repositories
454
+
455
+ ```ruby
456
+ require 'cuprum/collections/basic/repository'
457
+ ```
458
+
459
+ A `Basic::Repository` is a collection of `Basic::Collection`s. In addition to implementing the Repository methods (see [Repositories](#repositories), above), a basic repository can be initialized with a data set and used to build new collections directly.
460
+
461
+ ```ruby
462
+ data = {
463
+ 'books' => [
464
+ {
465
+ 'name' => 'Gideon the Ninth',
466
+ 'author' => 'Tamsyn Muir'
467
+ }
468
+ ]
469
+ }
470
+ repository = Cuprum::Collections::Basic::Repository.new(data: data)
471
+ repository.keys
472
+ #=> []
473
+
474
+ repository.build(collection_name: 'books')
475
+ #=> an instance of Cuprum::Collections::Basic::Collection
476
+ repository.keys
477
+ #=> ['books']
478
+ repository['books'].query.to_a
479
+ #=> [
480
+ # {
481
+ # 'name' => 'Gideon the Ninth',
482
+ # 'author' => 'Tamsyn Muir'
483
+ # }
484
+ # ]
485
+ ```
390
486
 
391
487
  <a id="constraints"></a>
392
488
 
@@ -686,7 +782,7 @@ The `#reset` method takes no parameters and returns the query. By default, a `Qu
686
782
  query.count
687
783
  #=> 10
688
784
 
689
- book = { id: 10, title: 'Gideon the Ninth', author: 'Tammsyn Muir' }
785
+ book = { id: 10, title: 'Gideon the Ninth', author: 'Tamsyn Muir' }
690
786
  collection.insert_one.call(entity: book)
691
787
 
692
788
  query.count
@@ -948,3 +1044,213 @@ query.each.map(&:title)
948
1044
  # 'The Farthest Shore'
949
1045
  # ]
950
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
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/basic'
4
+ require 'cuprum/collections/basic/collection'
5
+ require 'cuprum/collections/repository'
6
+
7
+ module Cuprum::Collections::Basic
8
+ # A repository represents a group of Basic collections.
9
+ class Repository < Cuprum::Collections::Repository
10
+ # @param data [Hash<String, Object>] Seed data to use when building
11
+ # collections.
12
+ def initialize(data: {})
13
+ super()
14
+
15
+ @data = data
16
+ end
17
+
18
+ # Adds a new collection with the given name to the repository.
19
+ #
20
+ # @param collection_name [String] The name of the new collection.
21
+ # @param data [Hash<String, Object>] The inital data for the collection. If
22
+ # not specified, defaults to the data used to initialize the repository.
23
+ # @param options [Hash] Additional options to pass to Collection.new
24
+ #
25
+ # @return [Cuprum::Collections::Basic::Collection] the created collection.
26
+ #
27
+ # @see Cuprum::Collections::Basic::Collection#initialize.
28
+ def build(collection_name:, data: nil, **options)
29
+ validate_collection_name!(collection_name)
30
+ validate_data!(data)
31
+
32
+ collection = Cuprum::Collections::Basic.new(
33
+ collection_name: collection_name,
34
+ data: data || @data.fetch(collection_name.to_s, []),
35
+ **options
36
+ )
37
+
38
+ add(collection)
39
+
40
+ collection
41
+ end
42
+
43
+ private
44
+
45
+ def valid_collection?(collection)
46
+ collection.is_a?(Cuprum::Collections::Basic::Collection)
47
+ end
48
+
49
+ def validate_collection_name!(name)
50
+ raise ArgumentError, "collection name can't be blank" if name.nil?
51
+
52
+ unless name.is_a?(String) || name.is_a?(Symbol)
53
+ raise ArgumentError, 'collection name must be a String or Symbol'
54
+ end
55
+
56
+ return unless name.empty?
57
+
58
+ raise ArgumentError, "collection name can't be blank"
59
+ end
60
+
61
+ def validate_data!(data)
62
+ return if data.nil? || data.is_a?(Array)
63
+
64
+ raise ArgumentError, 'data must be an Array of Hashes'
65
+ end
66
+ end
67
+ end