cuprum-collections 0.5.1 → 0.6.0.rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/lib/cuprum/collections/adaptable/collection.rb +18 -0
- data/lib/cuprum/collections/adaptable/command.rb +22 -0
- data/lib/cuprum/collections/adaptable/commands/abstract_assign_one.rb +27 -0
- data/lib/cuprum/collections/adaptable/commands/abstract_build_one.rb +25 -0
- data/lib/cuprum/collections/adaptable/commands/abstract_validate_one.rb +35 -0
- data/lib/cuprum/collections/adaptable/commands.rb +15 -0
- data/lib/cuprum/collections/adaptable/query.rb +64 -0
- data/lib/cuprum/collections/adaptable.rb +13 -0
- data/lib/cuprum/collections/adapter.rb +300 -0
- data/lib/cuprum/collections/adapters/data_adapter.rb +82 -0
- data/lib/cuprum/collections/adapters/entity_adapter.rb +76 -0
- data/lib/cuprum/collections/adapters/hash_adapter.rb +48 -0
- data/lib/cuprum/collections/adapters.rb +14 -0
- data/lib/cuprum/collections/basic/collection.rb +2 -20
- data/lib/cuprum/collections/basic/commands/destroy_one.rb +1 -1
- data/lib/cuprum/collections/basic/commands/find_many.rb +0 -31
- data/lib/cuprum/collections/basic/commands/find_matching.rb +0 -94
- data/lib/cuprum/collections/basic/commands/find_one.rb +0 -18
- data/lib/cuprum/collections/basic/commands/insert_one.rb +1 -1
- data/lib/cuprum/collections/basic/commands/update_one.rb +1 -1
- data/lib/cuprum/collections/basic/scopes/criteria_scope.rb +36 -21
- data/lib/cuprum/collections/basic.rb +6 -5
- data/lib/cuprum/collections/collection.rb +6 -0
- data/lib/cuprum/collections/collection_command.rb +1 -1
- data/lib/cuprum/collections/commands/abstract_find_many.rb +40 -3
- data/lib/cuprum/collections/commands/abstract_find_matching.rb +102 -0
- data/lib/cuprum/collections/commands/abstract_find_one.rb +23 -1
- data/lib/cuprum/collections/commands/associations/find_many.rb +1 -3
- data/lib/cuprum/collections/commands/associations/require_many.rb +1 -1
- data/lib/cuprum/collections/commands/find_one_matching.rb +10 -10
- data/lib/cuprum/collections/commands/query_command.rb +6 -4
- data/lib/cuprum/collections/commands/upsert.rb +0 -2
- data/lib/cuprum/collections/constraints/order/attributes_array.rb +5 -4
- data/lib/cuprum/collections/constraints/order/attributes_hash.rb +5 -4
- data/lib/cuprum/collections/constraints/order/sort_direction.rb +2 -2
- data/lib/cuprum/collections/constraints/ordering.rb +11 -9
- data/lib/cuprum/collections/constraints/query_hash.rb +2 -2
- data/lib/cuprum/collections/errors/abstract_find_error.rb +101 -23
- data/lib/cuprum/collections/errors/extra_attributes.rb +3 -3
- data/lib/cuprum/collections/errors/failed_validation.rb +3 -3
- data/lib/cuprum/collections/errors/missing_default_contract.rb +12 -4
- data/lib/cuprum/collections/queries.rb +4 -0
- data/lib/cuprum/collections/relation.rb +0 -2
- data/lib/cuprum/collections/relations/parameters.rb +120 -68
- data/lib/cuprum/collections/repository.rb +71 -6
- data/lib/cuprum/collections/rspec/contracts/query_contracts.rb +23 -4
- data/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +18 -0
- data/lib/cuprum/collections/rspec/contracts/scope_contracts.rb +51 -0
- data/lib/cuprum/collections/rspec/contracts/scopes/builder_contracts.rb +10 -0
- data/lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb +8 -0
- data/lib/cuprum/collections/rspec/contracts/scopes/criteria_contracts.rb +18 -366
- data/lib/cuprum/collections/rspec/contracts/scopes/logical_contracts.rb +30 -0
- data/lib/cuprum/collections/rspec/contracts/scopes.rb +2 -0
- data/lib/cuprum/collections/rspec/contracts.rb +2 -10
- data/lib/cuprum/collections/rspec/deferred/adapter_examples.rb +1077 -0
- data/lib/cuprum/collections/rspec/deferred/collection_examples.rb +27 -7
- data/lib/cuprum/collections/rspec/deferred/commands/assign_one_examples.rb +4 -4
- data/lib/cuprum/collections/rspec/deferred/commands/build_one_examples.rb +2 -2
- data/lib/cuprum/collections/rspec/deferred/commands/destroy_one_examples.rb +2 -2
- data/lib/cuprum/collections/rspec/deferred/commands/find_many_examples.rb +5 -5
- data/lib/cuprum/collections/rspec/deferred/commands/find_matching_examples.rb +45 -12
- data/lib/cuprum/collections/rspec/deferred/commands/find_one_examples.rb +2 -2
- data/lib/cuprum/collections/rspec/deferred/commands/insert_one_examples.rb +1 -1
- data/lib/cuprum/collections/rspec/deferred/commands/update_one_examples.rb +1 -1
- data/lib/cuprum/collections/rspec/deferred/query_examples.rb +930 -0
- data/lib/cuprum/collections/rspec/deferred/relation_examples.rb +48 -17
- data/lib/cuprum/collections/rspec/deferred/repository_examples.rb +961 -0
- data/lib/cuprum/collections/rspec/deferred/scope_examples.rb +598 -0
- data/lib/cuprum/collections/rspec/deferred/scopes/all_examples.rb +391 -0
- data/lib/cuprum/collections/rspec/deferred/scopes/builder_examples.rb +857 -0
- data/lib/cuprum/collections/rspec/deferred/scopes/composition_examples.rb +93 -0
- data/lib/cuprum/collections/rspec/deferred/scopes/conjunction_examples.rb +438 -0
- data/lib/cuprum/collections/rspec/deferred/scopes/criteria_examples.rb +1941 -0
- data/lib/cuprum/collections/rspec/deferred/scopes/disjunction_examples.rb +415 -0
- data/lib/cuprum/collections/rspec/deferred/scopes/none_examples.rb +385 -0
- data/lib/cuprum/collections/rspec/deferred/scopes/parser_examples.rb +740 -0
- data/lib/cuprum/collections/rspec/deferred/scopes.rb +8 -0
- data/lib/cuprum/collections/scope.rb +2 -2
- data/lib/cuprum/collections/scopes/container.rb +5 -4
- data/lib/cuprum/collections/scopes/criteria/parser.rb +24 -48
- data/lib/cuprum/collections/scopes/criteria.rb +7 -6
- data/lib/cuprum/collections/version.rb +4 -4
- data/lib/cuprum/collections.rb +5 -1
- metadata +47 -11
- data/lib/cuprum/collections/rspec/contracts/association_contracts.rb +0 -2127
- data/lib/cuprum/collections/rspec/contracts/basic.rb +0 -11
- data/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +0 -387
- data/lib/cuprum/collections/rspec/contracts/command_contracts.rb +0 -169
- data/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +0 -1264
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ac4327e0779a00b865a7f0ddc2e4f2cb6fddc28da8d459cff029b415ba4bd2a
|
|
4
|
+
data.tar.gz: e371fd9876bc8e4cf26ad5e2abb43588fe763fd37524c7bf32a12a34c83cf968
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '008fb96865a3264648f439054c7965ca81a386e00bf27bb67a193e981ba4ac044da8713ee09ac15d31b1d1ea47b20e46d3c1b29aa3fe4067f59589598da909b7'
|
|
7
|
+
data.tar.gz: 98fe25033606d342f72c6fe04c8eb6b351e1e970faeb4420ad969db2d28f25633dbeffa6a641de81a7caca0d2fe0672c77bd185eb209e7aeb3241398c2436260
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
Removed all deprecated functionality from version 0.5.0 and earlier.
|
|
6
|
+
|
|
7
|
+
### Collections
|
|
8
|
+
|
|
9
|
+
Added support for adaptable collections.
|
|
10
|
+
|
|
11
|
+
- Adapters map raw attributes from a datastore to a Ruby object.
|
|
12
|
+
- Added `Cuprum::Collections::Adapter`
|
|
13
|
+
- Added `Cuprum::Collections::Adapters::DataAdapter`
|
|
14
|
+
- Added `Cuprum::Collections::Adapters::EntityAdapter`
|
|
15
|
+
- Added `Cuprum::Collections::Adapters::HashAdapter`
|
|
16
|
+
- Adaptable collections use an adapter to transform the data into the desired object.
|
|
17
|
+
- Added `Cuprum::Collections::Adaptable::Collection`
|
|
18
|
+
- Added `Cuprum::Collections::Adaptable::Command`
|
|
19
|
+
- Added `Cuprum::Collections::Adaptable::Commands`
|
|
20
|
+
- Added `Cuprum::Collections::Adaptable::Query`
|
|
21
|
+
|
|
22
|
+
### Errors
|
|
23
|
+
|
|
24
|
+
Removed `#collection_name` from all Find errors. Either pass the `collection:` directly or pass collection parameters (`name:`, `entity_class:`, and/or `qualified_name:`).
|
|
25
|
+
|
|
26
|
+
### Queries
|
|
27
|
+
|
|
28
|
+
Added support for `NULL`, `NOT_NULL` operators in queries.
|
|
29
|
+
|
|
30
|
+
Added support for passing `Proc` values when parsing query criteria.
|
|
31
|
+
|
|
32
|
+
### Repositories
|
|
33
|
+
|
|
34
|
+
- Implemented `#find`, which finds the matching collection by name, qualified name, or entity class.
|
|
35
|
+
- Implemented `#remove`, which removes the collection with the specified qualified name.
|
|
36
|
+
- Deprecated `#find_or_create`. Use `#find` to find an existing collection. Use `#create` to add a new collection.
|
|
37
|
+
- Added support for passing a block to `Repository.new { |repository| }`.
|
|
38
|
+
|
|
39
|
+
### RSpec
|
|
40
|
+
|
|
41
|
+
Migrated shared contract objects to deferred example groups:
|
|
42
|
+
|
|
43
|
+
- `Cuprum::Collections::RSpec::Deferred::QueryExamples`
|
|
44
|
+
- `Cuprum::Collections::RSpec::Deferred::RepositoryExamples`
|
|
45
|
+
- `Cuprum::Collections::RSpec::Deferred::ScopeExamples`
|
|
46
|
+
- `Cuprum::Collections::RSpec::Deferred::Scopes::*`
|
|
47
|
+
|
|
48
|
+
The corresponding contracts are now deprecated.
|
|
49
|
+
|
|
3
50
|
## 0.5.1
|
|
4
51
|
|
|
5
52
|
Added missing `config/locales` directory to the gemspec.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/collections/adaptable'
|
|
4
|
+
|
|
5
|
+
module Cuprum::Collections::Adaptable
|
|
6
|
+
# Mixin for defining adaptable collections.
|
|
7
|
+
module Collection
|
|
8
|
+
# @param adapter [Cuprum::Collections::Adapter] the collection adapter.
|
|
9
|
+
def initialize(adapter:, **parameters)
|
|
10
|
+
super(default_entity_class: adapter.entity_class, **parameters)
|
|
11
|
+
|
|
12
|
+
@adapter = adapter
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Cuprum::Collections::Adapter] the collection adapter.
|
|
16
|
+
attr_reader :adapter
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/collections/adaptable'
|
|
4
|
+
|
|
5
|
+
module Cuprum::Collections::Adaptable
|
|
6
|
+
# Mixin for defining commands for adaptable collections.
|
|
7
|
+
module Command
|
|
8
|
+
# @return [Cuprum::Collections::Adapter] the adapter defined for the
|
|
9
|
+
# collection.
|
|
10
|
+
def adapter = collection.adapter
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def validate_attributes(attributes, as: 'attributes')
|
|
15
|
+
adapter.validate_attributes_parameter(attributes, as:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate_entity(entity, as: 'entity')
|
|
19
|
+
adapter.validate_entity_parameter(entity, as:)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/parameter_validation'
|
|
4
|
+
|
|
5
|
+
require 'cuprum/collections/adaptable/commands'
|
|
6
|
+
|
|
7
|
+
module Cuprum::Collections::Adaptable::Commands
|
|
8
|
+
# Abstract implementation of the AssignOne command for adaptable collections.
|
|
9
|
+
module AbstractAssignOne
|
|
10
|
+
include Cuprum::ParameterValidation
|
|
11
|
+
|
|
12
|
+
# @!method call(attributes:, entity:)
|
|
13
|
+
# Merges the given attributes into the given entity.
|
|
14
|
+
#
|
|
15
|
+
# @param attributes [Hash] the attributes to merge into the entity.
|
|
16
|
+
# @param entity [Object] the entity to update.
|
|
17
|
+
#
|
|
18
|
+
# @return [Object] an instance of the entity class with the updated
|
|
19
|
+
# attributes.
|
|
20
|
+
validate :attributes
|
|
21
|
+
validate :entity
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def process(attributes:, entity:) = adapter.merge(attributes:, entity:)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/parameter_validation'
|
|
4
|
+
|
|
5
|
+
require 'cuprum/collections/adaptable/commands'
|
|
6
|
+
|
|
7
|
+
module Cuprum::Collections::Adaptable::Commands
|
|
8
|
+
# Abstract implementation of the BuildOne command for adaptable collections.
|
|
9
|
+
module AbstractBuildOne
|
|
10
|
+
include Cuprum::ParameterValidation
|
|
11
|
+
|
|
12
|
+
# @!method call(attributes:)
|
|
13
|
+
# Creates a new entity from the given attributes.
|
|
14
|
+
#
|
|
15
|
+
# @param attributes [Hash] the attributes to build into an entity.
|
|
16
|
+
#
|
|
17
|
+
# @return [Object] an instance of the entity class with the given
|
|
18
|
+
# attributes.
|
|
19
|
+
validate :attributes
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def process(attributes:) = adapter.build(attributes:)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/parameter_validation'
|
|
4
|
+
|
|
5
|
+
require 'cuprum/collections/adaptable/commands'
|
|
6
|
+
|
|
7
|
+
module Cuprum::Collections::Adaptable::Commands
|
|
8
|
+
# Abstract, adaptable implementation of the ValidateOne command.
|
|
9
|
+
module AbstractValidateOne
|
|
10
|
+
include Cuprum::ParameterValidation
|
|
11
|
+
|
|
12
|
+
# @!method call(entity:, contract: nil)
|
|
13
|
+
# Validates the entity against the given or default contract.
|
|
14
|
+
#
|
|
15
|
+
# If the entity matches the contract, #call will return a passing result
|
|
16
|
+
# with the entity as the result value. If the entity does not match the
|
|
17
|
+
# contract, #call will return a failing result with a FailedValidation
|
|
18
|
+
# error and the validation errors.
|
|
19
|
+
#
|
|
20
|
+
# @param contract [Stannum::Constraints:Base] The contract with which to
|
|
21
|
+
# validate the entity. If not given, the entity will be validated using
|
|
22
|
+
# the collection's default contract.
|
|
23
|
+
# @param entity [Hash] The collection entity to validate.
|
|
24
|
+
#
|
|
25
|
+
# @return [Cuprum::Result<Hash>] the validated entity.
|
|
26
|
+
validate :contract, Stannum::Constraints::Base, optional: true
|
|
27
|
+
validate :entity
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def process(entity:, contract: nil)
|
|
32
|
+
adapter.validate(contract:, entity:)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/collections/adaptable'
|
|
4
|
+
|
|
5
|
+
module Cuprum::Collections::Adaptable
|
|
6
|
+
# Namespace for adaptable command implementations.
|
|
7
|
+
module Commands
|
|
8
|
+
autoload :AbstractAssignOne,
|
|
9
|
+
'cuprum/collections/adaptable/commands/abstract_assign_one'
|
|
10
|
+
autoload :AbstractBuildOne,
|
|
11
|
+
'cuprum/collections/adaptable/commands/abstract_build_one'
|
|
12
|
+
autoload :AbstractValidateOne,
|
|
13
|
+
'cuprum/collections/adaptable/commands/abstract_validate_one'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/collections/adaptable'
|
|
4
|
+
require 'cuprum/collections/query'
|
|
5
|
+
|
|
6
|
+
module Cuprum::Collections::Adaptable
|
|
7
|
+
# Mixin for adaptable collection Query implementations.
|
|
8
|
+
module Query
|
|
9
|
+
# Exception raised when the query cannot convert native data.
|
|
10
|
+
class AbstractQueryError < StandardError; end
|
|
11
|
+
|
|
12
|
+
# Exception raised when converting attributes returns a failing result.
|
|
13
|
+
class InvalidDataError < StandardError; end
|
|
14
|
+
|
|
15
|
+
# @param adapter [Cuprum::Collections::Adapter] the collection adapter.
|
|
16
|
+
def initialize(*, adapter:, **)
|
|
17
|
+
super(*, **)
|
|
18
|
+
|
|
19
|
+
@adapter = adapter
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Cuprum::Collections::Adapter] the collection adapter.
|
|
23
|
+
attr_reader :adapter
|
|
24
|
+
|
|
25
|
+
# Converts a native data representation to the adapter entity format.
|
|
26
|
+
#
|
|
27
|
+
# @param native [Object] the native representation of one collection item.
|
|
28
|
+
#
|
|
29
|
+
# @return [Object] the collection item in the format specified by the
|
|
30
|
+
# adapter.
|
|
31
|
+
def convert(native)
|
|
32
|
+
attributes = convert_native_to_attributes(native)
|
|
33
|
+
result = adapter.build(attributes:)
|
|
34
|
+
|
|
35
|
+
return result.value if result.success?
|
|
36
|
+
|
|
37
|
+
# This is an internal data error, not resolvable by the user.
|
|
38
|
+
raise InvalidDataError,
|
|
39
|
+
invalid_data_error_message(attributes:, error: result.error, native:)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def convert_native_to_attributes(_)
|
|
45
|
+
raise AbstractQueryError,
|
|
46
|
+
"#{self.class.name} is an abstract class - define a subclass and " \
|
|
47
|
+
'implement the #convert_native_to_attributes method'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def invalid_data_error_message(attributes:, error:, native:)
|
|
51
|
+
message = 'Unable to process query data'
|
|
52
|
+
|
|
53
|
+
if error
|
|
54
|
+
message += " - #{error.message}" if error.message
|
|
55
|
+
message += "\n error details: #{error.as_json}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
message += "\n raw data: #{native.inspect}"
|
|
59
|
+
message += "\n attributes: #{attributes.inspect}"
|
|
60
|
+
|
|
61
|
+
message
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/collections'
|
|
4
|
+
|
|
5
|
+
module Cuprum::Collections
|
|
6
|
+
# Namespace for defining adaptable collections.
|
|
7
|
+
module Adaptable
|
|
8
|
+
autoload :Collection, 'cuprum/collections/adaptable/collection'
|
|
9
|
+
autoload :Command, 'cuprum/collections/adaptable/command'
|
|
10
|
+
autoload :Commands, 'cuprum/collections/adaptable/commands'
|
|
11
|
+
autoload :Query, 'cuprum/collections/adaptable/query'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum'
|
|
4
|
+
require 'cuprum/result_helpers'
|
|
5
|
+
|
|
6
|
+
require 'cuprum/collections'
|
|
7
|
+
|
|
8
|
+
module Cuprum::Collections
|
|
9
|
+
# Utility class for converting between raw attributes and a data format.
|
|
10
|
+
class Adapter # rubocop:disable Metrics/ClassLength
|
|
11
|
+
include Cuprum::ResultHelpers
|
|
12
|
+
include Cuprum::Steps
|
|
13
|
+
|
|
14
|
+
# @param options [Hash] options for initializing the adapter.
|
|
15
|
+
#
|
|
16
|
+
# @option options allow_extra_attributes [true, false] if false, attributes
|
|
17
|
+
# methods return an error for attributes not in attributes_names. Defaults
|
|
18
|
+
# to true if attribute_names is empty, otherwise false.
|
|
19
|
+
# @option options attributes_names [Array<String, Symbol>] the valid
|
|
20
|
+
# attribute names for a data object. Defaults to [].
|
|
21
|
+
# @option options default_contract [Stannum::Constraints:Base] the contract
|
|
22
|
+
# used to validate instances of the data object.
|
|
23
|
+
# @option options entity_class [Class] the class of the data objects.
|
|
24
|
+
def initialize(**options) # rubocop:disable Metrics/MethodLength
|
|
25
|
+
@attribute_names =
|
|
26
|
+
options
|
|
27
|
+
.fetch(:attribute_names, [])
|
|
28
|
+
.compact
|
|
29
|
+
.map(&:to_s)
|
|
30
|
+
.then { |ary| Set.new(ary) }
|
|
31
|
+
@default_contract = options[:default_contract]
|
|
32
|
+
@entity_class = options[:entity_class]
|
|
33
|
+
@allow_extra_attributes =
|
|
34
|
+
options.fetch(:allow_extra_attributes, @attribute_names.empty?)
|
|
35
|
+
|
|
36
|
+
validate_entity_class_parameter(@entity_class)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Set<String>] the valid attribute names for a data object.
|
|
40
|
+
attr_reader :attribute_names
|
|
41
|
+
|
|
42
|
+
# @return [Stannum::Constraints:Base] the contract used to validate
|
|
43
|
+
# instances of the data object.
|
|
44
|
+
attr_reader :default_contract
|
|
45
|
+
|
|
46
|
+
# @return [Class] the class of the data objects.
|
|
47
|
+
attr_reader :entity_class
|
|
48
|
+
|
|
49
|
+
# @return [true, false] if false, attributes methods return an error for
|
|
50
|
+
# attributes not in attributes_names.
|
|
51
|
+
def allow_extra_attributes?
|
|
52
|
+
@allow_extra_attributes
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Generates a data object from an attributes hash.
|
|
56
|
+
#
|
|
57
|
+
# @param attributes [Hash] the attributes used to initialize the object.
|
|
58
|
+
#
|
|
59
|
+
# @return [Cuprum::Result<Object>] the result with the generated object or
|
|
60
|
+
# the error.
|
|
61
|
+
def build(attributes:)
|
|
62
|
+
steps do
|
|
63
|
+
handle_invalid_parameters(
|
|
64
|
+
*validate_attributes_parameter(attributes)
|
|
65
|
+
)
|
|
66
|
+
handle_extra_attributes(attributes)
|
|
67
|
+
|
|
68
|
+
build_entity(attributes:)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns a data object with updated attributes.
|
|
73
|
+
#
|
|
74
|
+
# @param attributes [Hash] the attributes used to update the object.
|
|
75
|
+
# @param entity [Object] the data object to update.
|
|
76
|
+
#
|
|
77
|
+
# @return [Cuprum::Result<Object>] the result with the updated object or the
|
|
78
|
+
# error.
|
|
79
|
+
def merge(attributes:, entity:)
|
|
80
|
+
steps do
|
|
81
|
+
handle_invalid_parameters(
|
|
82
|
+
*validate_attributes_parameter(attributes),
|
|
83
|
+
validate_entity_parameter(entity)
|
|
84
|
+
)
|
|
85
|
+
handle_extra_attributes(attributes)
|
|
86
|
+
|
|
87
|
+
merge_entity(attributes:, entity:)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Generates an attributes hash from a data object.
|
|
92
|
+
#
|
|
93
|
+
# @param entity [Object] the data object to serialize.
|
|
94
|
+
#
|
|
95
|
+
# @return [Cuprum::Result<Object>] the result with the generated attributes
|
|
96
|
+
# or the error.
|
|
97
|
+
def serialize(entity:)
|
|
98
|
+
steps do
|
|
99
|
+
handle_invalid_parameters(
|
|
100
|
+
validate_entity_parameter(entity)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
serialize_entity(entity:)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Validates a data object.
|
|
108
|
+
#
|
|
109
|
+
# @param entity [Object] the data object to validate.
|
|
110
|
+
# @param contract [Stannum::Constraint] the contract used to validate the
|
|
111
|
+
# data object, if any.
|
|
112
|
+
#
|
|
113
|
+
# @return [Cuprum::Result<Object>] a passing result with the data object, or
|
|
114
|
+
# the error if the data object is not valid.
|
|
115
|
+
def validate(entity:, contract: nil)
|
|
116
|
+
steps do
|
|
117
|
+
handle_invalid_parameters(
|
|
118
|
+
validate_entity_parameter(entity)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
validate_entity(contract:, entity:)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Asserts that an attributes parameter is a Hash with valid keys.
|
|
126
|
+
#
|
|
127
|
+
# @param attributes [Object] the attributes to validate.
|
|
128
|
+
# @param as [String] the name of the validate object. Defaults to
|
|
129
|
+
# "attributes".
|
|
130
|
+
#
|
|
131
|
+
# @return [String, nil] the error message if the attributes are not a Hash
|
|
132
|
+
# with valid keys; or nil if the attributes are valid.
|
|
133
|
+
def validate_attributes_parameter(attributes, as: 'attributes')
|
|
134
|
+
return attributes_not_hash_error(as:) unless attributes.is_a?(Hash)
|
|
135
|
+
|
|
136
|
+
attributes
|
|
137
|
+
.each_key
|
|
138
|
+
.with_object([]) do |key, messages|
|
|
139
|
+
messages << validate_attributes_parameter_key(
|
|
140
|
+
key,
|
|
141
|
+
as: "#{as}[#{key.inspect}] key"
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
.compact
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Asserts that an entity parameter is of valid type.
|
|
148
|
+
#
|
|
149
|
+
# @param entity [Object] the entity to validate.
|
|
150
|
+
# @param as [String] the name of the validated object. Defaults to "entity".
|
|
151
|
+
#
|
|
152
|
+
# @return [String, nil] the error message if the entity is not of valid
|
|
153
|
+
# type; or nil if the entity is valid.
|
|
154
|
+
def validate_entity_parameter(entity, as: 'entity')
|
|
155
|
+
return unless entity_class
|
|
156
|
+
|
|
157
|
+
return if entity.is_a?(entity_class)
|
|
158
|
+
|
|
159
|
+
tools.assertions.error_message_for(
|
|
160
|
+
'sleeping_king_studios.tools.assertions.instance_of',
|
|
161
|
+
as:,
|
|
162
|
+
expected: entity_class
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def attributes_not_hash_error(as:)
|
|
169
|
+
tools.assertions.error_message_for(
|
|
170
|
+
'sleeping_king_studios.tools.assertions.instance_of',
|
|
171
|
+
as:,
|
|
172
|
+
expected: Hash
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def build_entity(**)
|
|
177
|
+
failure(not_implemented_error)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def default_contract_for(**)
|
|
181
|
+
default_contract
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def empty_attribute_key_error(as:)
|
|
185
|
+
tools.assertions.error_message_for(
|
|
186
|
+
'sleeping_king_studios.tools.assertions.presence',
|
|
187
|
+
as:
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def extra_attributes_error(extra_attributes:)
|
|
192
|
+
Cuprum::Collections::Errors::ExtraAttributes.new(
|
|
193
|
+
entity_class:,
|
|
194
|
+
extra_attributes:,
|
|
195
|
+
valid_attributes: attribute_names.to_a
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def failed_validation_error(errors:)
|
|
200
|
+
Cuprum::Collections::Errors::FailedValidation.new(
|
|
201
|
+
entity_class:,
|
|
202
|
+
errors:
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def handle_extra_attributes(attributes)
|
|
207
|
+
step do
|
|
208
|
+
return if allow_extra_attributes?
|
|
209
|
+
|
|
210
|
+
extra_attributes = attributes.each_key.reject do |key|
|
|
211
|
+
key = key.to_s if key.is_a?(Symbol)
|
|
212
|
+
|
|
213
|
+
attribute_names.include?(key)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
return if extra_attributes.empty?
|
|
217
|
+
|
|
218
|
+
failure(extra_attributes_error(extra_attributes:))
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def handle_invalid_parameters(*messages)
|
|
223
|
+
step do
|
|
224
|
+
failures = messages.compact.reject(&:empty?)
|
|
225
|
+
|
|
226
|
+
return if failures.empty?
|
|
227
|
+
|
|
228
|
+
failures = failures.map { |failure| failure.split(', ') }.flatten
|
|
229
|
+
|
|
230
|
+
failure(invalid_parameters_error(failures:))
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def invalid_attribute_key_error(as:)
|
|
235
|
+
tools.assertions.error_message_for(
|
|
236
|
+
'sleeping_king_studios.tools.assertions.name',
|
|
237
|
+
as:
|
|
238
|
+
)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def invalid_parameters_error(failures:)
|
|
242
|
+
Cuprum::Errors::InvalidParameters
|
|
243
|
+
.new(command_class: self.class, failures:)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def match_contract(contract:, entity:)
|
|
247
|
+
return contract.match(entity) if contract
|
|
248
|
+
|
|
249
|
+
match_native_validation(entity:)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def match_native_validation(**)
|
|
253
|
+
failure(missing_default_contract_error)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def merge_entity(**)
|
|
257
|
+
failure(not_implemented_error)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def missing_default_contract_error
|
|
261
|
+
Cuprum::Collections::Errors::MissingDefaultContract.new(entity_class:)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def not_implemented_error
|
|
265
|
+
Cuprum::Errors::CommandNotImplemented.new(command: self)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def serialize_entity(**)
|
|
269
|
+
failure(not_implemented_error)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def tools
|
|
273
|
+
SleepingKingStudios::Tools::Toolbelt.instance
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def validate_attributes_parameter_key(key, as:)
|
|
277
|
+
return empty_attribute_key_error(as:) if key.nil?
|
|
278
|
+
|
|
279
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
280
|
+
return invalid_attribute_key_error(as:)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
return unless key.empty?
|
|
284
|
+
|
|
285
|
+
empty_attribute_key_error(as:)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def validate_entity(contract:, entity:)
|
|
289
|
+
contract ||= default_contract_for(entity:)
|
|
290
|
+
|
|
291
|
+
match, errors = step { match_contract(contract:, entity:) }
|
|
292
|
+
|
|
293
|
+
return success(entity) if match
|
|
294
|
+
|
|
295
|
+
failure(failed_validation_error(errors:))
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def validate_entity_class_parameter(*, **) = nil
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cuprum/collections/adapter'
|
|
4
|
+
require 'cuprum/collections/adapters'
|
|
5
|
+
|
|
6
|
+
module Cuprum::Collections::Adapters
|
|
7
|
+
# Utility class for converting between raw attributes and a Data class.
|
|
8
|
+
class DataAdapter < Cuprum::Collections::Adapter
|
|
9
|
+
# @param options [Hash] options for initializing the adapter.
|
|
10
|
+
#
|
|
11
|
+
# @option options attributes_names [Array<String, Symbol>] the valid
|
|
12
|
+
# attribute names for a data object. Defaults to the entity class's
|
|
13
|
+
# members. Must be a subset of the entity class's members.
|
|
14
|
+
# @option options default_contract [Stannum::Constraints:Base] the contract
|
|
15
|
+
# used to validate instances of the data object.
|
|
16
|
+
# @option options entity_class [Class] the class of the data objects. Must
|
|
17
|
+
# be a Data subclass.
|
|
18
|
+
def initialize(entity_class:, **options)
|
|
19
|
+
if options[:allow_extra_attributes]
|
|
20
|
+
raise ArgumentError, 'adapter does not support extra attributes'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attribute_names = options.fetch(:attribute_names) do
|
|
24
|
+
data_class?(entity_class) ? entity_class.members : []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
super(attribute_names:, entity_class:, **options)
|
|
28
|
+
|
|
29
|
+
verify_attribute_names_are_members
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def build_entity(attributes:)
|
|
35
|
+
attributes = empty_attributes.merge(attributes)
|
|
36
|
+
|
|
37
|
+
entity_class.new(**attributes)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def data_class?(entity_class)
|
|
41
|
+
entity_class.is_a?(Class) && entity_class < Data
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def empty_attributes
|
|
45
|
+
@empty_attributes ||= member_names.to_h { |key| [key, nil] }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def member_names
|
|
49
|
+
@member_names ||= entity_class.members.map(&:to_s)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def merge_entity(attributes:, entity:)
|
|
53
|
+
attributes = entity.to_h.merge(attributes)
|
|
54
|
+
|
|
55
|
+
entity_class.new(**attributes)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def serialize_entity(entity:)
|
|
59
|
+
tools.hash_tools.convert_keys_to_strings(entity.to_h)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def validate_entity_class_parameter(entity_class, as: 'entity class')
|
|
63
|
+
tools.assertions.validate_class(entity_class, as:)
|
|
64
|
+
|
|
65
|
+
return if entity_class < Data
|
|
66
|
+
|
|
67
|
+
raise ArgumentError, "#{as} is not a subclass of Data"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def verify_attribute_names_are_members
|
|
71
|
+
invalid_names = attribute_names - member_names
|
|
72
|
+
|
|
73
|
+
return if invalid_names.empty?
|
|
74
|
+
|
|
75
|
+
error_message =
|
|
76
|
+
"attribute names #{invalid_names.join(', ')} are not members of " \
|
|
77
|
+
"#{entity_class.name || 'the entity class'}"
|
|
78
|
+
|
|
79
|
+
raise ArgumentError, error_message
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|