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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/lib/cuprum/collections/adaptable/collection.rb +18 -0
  4. data/lib/cuprum/collections/adaptable/command.rb +22 -0
  5. data/lib/cuprum/collections/adaptable/commands/abstract_assign_one.rb +27 -0
  6. data/lib/cuprum/collections/adaptable/commands/abstract_build_one.rb +25 -0
  7. data/lib/cuprum/collections/adaptable/commands/abstract_validate_one.rb +35 -0
  8. data/lib/cuprum/collections/adaptable/commands.rb +15 -0
  9. data/lib/cuprum/collections/adaptable/query.rb +64 -0
  10. data/lib/cuprum/collections/adaptable.rb +13 -0
  11. data/lib/cuprum/collections/adapter.rb +300 -0
  12. data/lib/cuprum/collections/adapters/data_adapter.rb +82 -0
  13. data/lib/cuprum/collections/adapters/entity_adapter.rb +76 -0
  14. data/lib/cuprum/collections/adapters/hash_adapter.rb +48 -0
  15. data/lib/cuprum/collections/adapters.rb +14 -0
  16. data/lib/cuprum/collections/basic/collection.rb +2 -20
  17. data/lib/cuprum/collections/basic/commands/destroy_one.rb +1 -1
  18. data/lib/cuprum/collections/basic/commands/find_many.rb +0 -31
  19. data/lib/cuprum/collections/basic/commands/find_matching.rb +0 -94
  20. data/lib/cuprum/collections/basic/commands/find_one.rb +0 -18
  21. data/lib/cuprum/collections/basic/commands/insert_one.rb +1 -1
  22. data/lib/cuprum/collections/basic/commands/update_one.rb +1 -1
  23. data/lib/cuprum/collections/basic/scopes/criteria_scope.rb +36 -21
  24. data/lib/cuprum/collections/basic.rb +6 -5
  25. data/lib/cuprum/collections/collection.rb +6 -0
  26. data/lib/cuprum/collections/collection_command.rb +1 -1
  27. data/lib/cuprum/collections/commands/abstract_find_many.rb +40 -3
  28. data/lib/cuprum/collections/commands/abstract_find_matching.rb +102 -0
  29. data/lib/cuprum/collections/commands/abstract_find_one.rb +23 -1
  30. data/lib/cuprum/collections/commands/associations/find_many.rb +1 -3
  31. data/lib/cuprum/collections/commands/associations/require_many.rb +1 -1
  32. data/lib/cuprum/collections/commands/find_one_matching.rb +10 -10
  33. data/lib/cuprum/collections/commands/query_command.rb +6 -4
  34. data/lib/cuprum/collections/commands/upsert.rb +0 -2
  35. data/lib/cuprum/collections/constraints/order/attributes_array.rb +5 -4
  36. data/lib/cuprum/collections/constraints/order/attributes_hash.rb +5 -4
  37. data/lib/cuprum/collections/constraints/order/sort_direction.rb +2 -2
  38. data/lib/cuprum/collections/constraints/ordering.rb +11 -9
  39. data/lib/cuprum/collections/constraints/query_hash.rb +2 -2
  40. data/lib/cuprum/collections/errors/abstract_find_error.rb +101 -23
  41. data/lib/cuprum/collections/errors/extra_attributes.rb +3 -3
  42. data/lib/cuprum/collections/errors/failed_validation.rb +3 -3
  43. data/lib/cuprum/collections/errors/missing_default_contract.rb +12 -4
  44. data/lib/cuprum/collections/queries.rb +4 -0
  45. data/lib/cuprum/collections/relation.rb +0 -2
  46. data/lib/cuprum/collections/relations/parameters.rb +120 -68
  47. data/lib/cuprum/collections/repository.rb +71 -6
  48. data/lib/cuprum/collections/rspec/contracts/query_contracts.rb +23 -4
  49. data/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +18 -0
  50. data/lib/cuprum/collections/rspec/contracts/scope_contracts.rb +51 -0
  51. data/lib/cuprum/collections/rspec/contracts/scopes/builder_contracts.rb +10 -0
  52. data/lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb +8 -0
  53. data/lib/cuprum/collections/rspec/contracts/scopes/criteria_contracts.rb +18 -366
  54. data/lib/cuprum/collections/rspec/contracts/scopes/logical_contracts.rb +30 -0
  55. data/lib/cuprum/collections/rspec/contracts/scopes.rb +2 -0
  56. data/lib/cuprum/collections/rspec/contracts.rb +2 -10
  57. data/lib/cuprum/collections/rspec/deferred/adapter_examples.rb +1077 -0
  58. data/lib/cuprum/collections/rspec/deferred/collection_examples.rb +27 -7
  59. data/lib/cuprum/collections/rspec/deferred/commands/assign_one_examples.rb +4 -4
  60. data/lib/cuprum/collections/rspec/deferred/commands/build_one_examples.rb +2 -2
  61. data/lib/cuprum/collections/rspec/deferred/commands/destroy_one_examples.rb +2 -2
  62. data/lib/cuprum/collections/rspec/deferred/commands/find_many_examples.rb +5 -5
  63. data/lib/cuprum/collections/rspec/deferred/commands/find_matching_examples.rb +45 -12
  64. data/lib/cuprum/collections/rspec/deferred/commands/find_one_examples.rb +2 -2
  65. data/lib/cuprum/collections/rspec/deferred/commands/insert_one_examples.rb +1 -1
  66. data/lib/cuprum/collections/rspec/deferred/commands/update_one_examples.rb +1 -1
  67. data/lib/cuprum/collections/rspec/deferred/query_examples.rb +930 -0
  68. data/lib/cuprum/collections/rspec/deferred/relation_examples.rb +48 -17
  69. data/lib/cuprum/collections/rspec/deferred/repository_examples.rb +961 -0
  70. data/lib/cuprum/collections/rspec/deferred/scope_examples.rb +598 -0
  71. data/lib/cuprum/collections/rspec/deferred/scopes/all_examples.rb +391 -0
  72. data/lib/cuprum/collections/rspec/deferred/scopes/builder_examples.rb +857 -0
  73. data/lib/cuprum/collections/rspec/deferred/scopes/composition_examples.rb +93 -0
  74. data/lib/cuprum/collections/rspec/deferred/scopes/conjunction_examples.rb +438 -0
  75. data/lib/cuprum/collections/rspec/deferred/scopes/criteria_examples.rb +1941 -0
  76. data/lib/cuprum/collections/rspec/deferred/scopes/disjunction_examples.rb +415 -0
  77. data/lib/cuprum/collections/rspec/deferred/scopes/none_examples.rb +385 -0
  78. data/lib/cuprum/collections/rspec/deferred/scopes/parser_examples.rb +740 -0
  79. data/lib/cuprum/collections/rspec/deferred/scopes.rb +8 -0
  80. data/lib/cuprum/collections/scope.rb +2 -2
  81. data/lib/cuprum/collections/scopes/container.rb +5 -4
  82. data/lib/cuprum/collections/scopes/criteria/parser.rb +24 -48
  83. data/lib/cuprum/collections/scopes/criteria.rb +7 -6
  84. data/lib/cuprum/collections/version.rb +4 -4
  85. data/lib/cuprum/collections.rb +5 -1
  86. metadata +47 -11
  87. data/lib/cuprum/collections/rspec/contracts/association_contracts.rb +0 -2127
  88. data/lib/cuprum/collections/rspec/contracts/basic.rb +0 -11
  89. data/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +0 -387
  90. data/lib/cuprum/collections/rspec/contracts/command_contracts.rb +0 -169
  91. 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: c5a70ba7c1dd93657afb1f94f345b6d422e1fee51d7845b249cd2b14e5da387f
4
- data.tar.gz: 17121928330b174b661ca5e03a727a8976ddce5bc977f3cab90a490c615dfd0c
3
+ metadata.gz: 2ac4327e0779a00b865a7f0ddc2e4f2cb6fddc28da8d459cff029b415ba4bd2a
4
+ data.tar.gz: e371fd9876bc8e4cf26ad5e2abb43588fe763fd37524c7bf32a12a34c83cf968
5
5
  SHA512:
6
- metadata.gz: b871c8cfa148ff76fb46dc1bc59b07e1f4db4ec71eec23b7194a694fba5fb4db48b3f4c9e96c45de299140b556ff0af337b40b3f41752fa350fb64b0369e789a
7
- data.tar.gz: d9ca82d7b9cead80d22ca4b003da62c1f3add4e3e4a8fc957b4c8d6db5796316e4f8267bed744e2762708f52e34565aafad6778bafc6517e8efa59b2933596de
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