cuprum-collections 0.3.0 → 0.4.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/DEVELOPMENT.md +2 -2
  4. data/README.md +13 -11
  5. data/lib/cuprum/collections/association.rb +256 -0
  6. data/lib/cuprum/collections/associations/belongs_to.rb +32 -0
  7. data/lib/cuprum/collections/associations/has_many.rb +23 -0
  8. data/lib/cuprum/collections/associations/has_one.rb +23 -0
  9. data/lib/cuprum/collections/associations.rb +10 -0
  10. data/lib/cuprum/collections/basic/collection.rb +39 -74
  11. data/lib/cuprum/collections/basic/commands/find_many.rb +1 -1
  12. data/lib/cuprum/collections/basic/commands/find_matching.rb +1 -1
  13. data/lib/cuprum/collections/basic/repository.rb +9 -33
  14. data/lib/cuprum/collections/basic.rb +1 -0
  15. data/lib/cuprum/collections/collection.rb +154 -0
  16. data/lib/cuprum/collections/commands/associations/find_many.rb +161 -0
  17. data/lib/cuprum/collections/commands/associations/require_many.rb +48 -0
  18. data/lib/cuprum/collections/commands/associations.rb +13 -0
  19. data/lib/cuprum/collections/commands/find_one_matching.rb +1 -1
  20. data/lib/cuprum/collections/commands.rb +1 -0
  21. data/lib/cuprum/collections/errors/abstract_find_error.rb +1 -1
  22. data/lib/cuprum/collections/relation.rb +401 -0
  23. data/lib/cuprum/collections/repository.rb +71 -4
  24. data/lib/cuprum/collections/resource.rb +65 -0
  25. data/lib/cuprum/collections/rspec/contracts/association_contracts.rb +2137 -0
  26. data/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb +484 -0
  27. data/lib/cuprum/collections/rspec/contracts/basic.rb +11 -0
  28. data/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +429 -0
  29. data/lib/cuprum/collections/rspec/contracts/command_contracts.rb +1462 -0
  30. data/lib/cuprum/collections/rspec/contracts/query_contracts.rb +1093 -0
  31. data/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +1381 -0
  32. data/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +605 -0
  33. data/lib/cuprum/collections/rspec/contracts.rb +23 -0
  34. data/lib/cuprum/collections/rspec/fixtures.rb +85 -82
  35. data/lib/cuprum/collections/rspec.rb +4 -1
  36. data/lib/cuprum/collections/version.rb +1 -1
  37. data/lib/cuprum/collections.rb +9 -4
  38. metadata +23 -19
  39. data/lib/cuprum/collections/base.rb +0 -11
  40. data/lib/cuprum/collections/basic/rspec/command_contract.rb +0 -392
  41. data/lib/cuprum/collections/rspec/assign_one_command_contract.rb +0 -168
  42. data/lib/cuprum/collections/rspec/build_one_command_contract.rb +0 -93
  43. data/lib/cuprum/collections/rspec/collection_contract.rb +0 -190
  44. data/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +0 -108
  45. data/lib/cuprum/collections/rspec/find_many_command_contract.rb +0 -407
  46. data/lib/cuprum/collections/rspec/find_matching_command_contract.rb +0 -194
  47. data/lib/cuprum/collections/rspec/find_one_command_contract.rb +0 -157
  48. data/lib/cuprum/collections/rspec/insert_one_command_contract.rb +0 -84
  49. data/lib/cuprum/collections/rspec/query_builder_contract.rb +0 -92
  50. data/lib/cuprum/collections/rspec/query_contract.rb +0 -650
  51. data/lib/cuprum/collections/rspec/querying_contract.rb +0 -298
  52. data/lib/cuprum/collections/rspec/repository_contract.rb +0 -235
  53. data/lib/cuprum/collections/rspec/update_one_command_contract.rb +0 -80
  54. data/lib/cuprum/collections/rspec/validate_one_command_contract.rb +0 -96
@@ -15,49 +15,25 @@ module Cuprum::Collections::Basic
15
15
  @data = data
16
16
  end
17
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)
18
+ private
19
+
20
+ def build_collection(data: nil, **parameters)
30
21
  validate_data!(data)
31
22
 
32
- collection = Cuprum::Collections::Basic.new(
33
- collection_name: collection_name,
34
- data: data || @data.fetch(collection_name.to_s, []),
35
- **options
36
- )
23
+ qualified_name =
24
+ Cuprum::Collections::Relation::Disambiguation
25
+ .resolve_parameters(parameters, name: :collection_name)
26
+ .fetch(:qualified_name)
37
27
 
38
- add(collection)
28
+ data ||= @data.fetch(qualified_name, [])
39
29
 
40
- collection
30
+ Cuprum::Collections::Basic.new(data: data, **parameters)
41
31
  end
42
32
 
43
- private
44
-
45
33
  def valid_collection?(collection)
46
34
  collection.is_a?(Cuprum::Collections::Basic::Collection)
47
35
  end
48
36
 
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
37
  def validate_data!(data)
62
38
  return if data.nil? || data.is_a?(Array)
63
39
 
@@ -18,5 +18,6 @@ module Cuprum::Collections
18
18
  autoload :Command, 'cuprum/collections/basic/command'
19
19
  autoload :Commands, 'cuprum/collections/basic/commands'
20
20
  autoload :Query, 'cuprum/collections/basic/query'
21
+ autoload :Repository, 'cuprum/collections/basic/repository'
21
22
  end
22
23
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/command_factory'
4
+
5
+ require 'cuprum/collections'
6
+ require 'cuprum/collections/relation'
7
+
8
+ module Cuprum::Collections
9
+ # Provides a base implementation for collections.
10
+ class Collection < Cuprum::CommandFactory
11
+ include Cuprum::Collections::Relation::Parameters
12
+ include Cuprum::Collections::Relation::PrimaryKeys
13
+ include Cuprum::Collections::Relation::Disambiguation
14
+
15
+ # Error raised when trying to call an abstract collection method.
16
+ class AbstractCollectionError < StandardError; end
17
+
18
+ IGNORED_PARAMETERS = %i[
19
+ collection_name
20
+ entity_class
21
+ member_name
22
+ name
23
+ qualified_name
24
+ singular_name
25
+ ].freeze
26
+ private_constant :IGNORED_PARAMETERS
27
+
28
+ # @overload initialize(entity_class: nil, name: nil, qualified_name: nil, singular_name: nil, **options)
29
+ # @param entity_class [Class, String] the class of entity represented by
30
+ # the relation.
31
+ # @param name [String] the name of the relation.
32
+ # @param qualified_name [String] a scoped name for the relation.
33
+ # @param singular_name [String] the name of an entity in the relation.
34
+ # @param options [Hash] additional options for the relation.
35
+ #
36
+ # @option options primary_key_name [String] the name of the primary key
37
+ # attribute. Defaults to 'id'.
38
+ # @option primary_key_type [Class, Stannum::Constraint] the type of
39
+ # the primary key attribute. Defaults to Integer.
40
+ def initialize(**parameters) # rubocop:disable Metrics/MethodLength
41
+ super()
42
+
43
+ relation_params = resolve_parameters(
44
+ parameters,
45
+ name: :collection_name,
46
+ singular_name: :member_name
47
+ )
48
+ @entity_class = relation_params[:entity_class]
49
+ @name = relation_params[:name]
50
+ @plural_name = relation_params[:plural_name]
51
+ @qualified_name = relation_params[:qualified_name]
52
+ @singular_name = relation_params[:singular_name]
53
+
54
+ @options = ignore_parameters(**parameters)
55
+ end
56
+
57
+ # @return [Hash<Symbol>] additional options for the collection.
58
+ attr_reader :options
59
+
60
+ # @param other [Object] The object to compare.
61
+ #
62
+ # @return [true, false] true if the other object is a collection with the
63
+ # same options, otherwise false.
64
+ def ==(other)
65
+ return false unless self.class == other.class
66
+
67
+ comparable_options == other.comparable_options
68
+ end
69
+
70
+ # @return [String] the name of the collection.
71
+ def collection_name
72
+ tools.core_tools.deprecate '#collection_name method',
73
+ message: 'Use #name instead'
74
+
75
+ name
76
+ end
77
+
78
+ # @return [Integer] the count of items in the collection.
79
+ def count
80
+ query.count
81
+ end
82
+ alias size count
83
+
84
+ # Checks if the collection matches the expected options.
85
+ #
86
+ # @param expected [Hash] the options to compare.
87
+ #
88
+ # @return [Boolean] true if all of the expected options match, otherwise
89
+ # false.
90
+ def matches?(**expected)
91
+ if expected[:entity_class].is_a?(String)
92
+ expected = expected.merge(
93
+ entity_class: Object.const_get(expected[:entity_class])
94
+ )
95
+ end
96
+
97
+ comparable_options >= expected
98
+ end
99
+
100
+ # @return [String] the name of an entity in the relation.
101
+ def member_name
102
+ tools.core_tools.deprecate '#member_name method',
103
+ message: 'Use #singular_name instead'
104
+
105
+ singular_name
106
+ end
107
+
108
+ # A new Query instance, used for querying against the collection data.
109
+ #
110
+ # @return [Object] the query.
111
+ def query
112
+ raise AbstractCollectionError,
113
+ "#{self.class.name} is an abstract class. Define a repository " \
114
+ 'subclass and implement the #query method.'
115
+ end
116
+
117
+ protected
118
+
119
+ def comparable_options
120
+ command_options.merge(
121
+ name: name,
122
+ qualified_name: qualified_name,
123
+ singular_name: singular_name
124
+ )
125
+ end
126
+
127
+ private
128
+
129
+ def command_options
130
+ @command_options ||= {
131
+ collection_name: name,
132
+ entity_class: entity_class,
133
+ member_name: singular_name,
134
+ primary_key_name: primary_key_name,
135
+ primary_key_type: primary_key_type,
136
+ **options
137
+ }
138
+ end
139
+
140
+ def ignore_parameters(**parameters)
141
+ parameters
142
+ .reject { |key, _| ignored_parameters.include?(key) }
143
+ .to_h
144
+ end
145
+
146
+ def ignored_parameters
147
+ @ignored_parameters ||= Set.new(IGNORED_PARAMETERS)
148
+ end
149
+
150
+ def tools
151
+ SleepingKingStudios::Tools::Toolbelt.instance
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/commands/associations'
4
+
5
+ module Cuprum::Collections::Commands::Associations
6
+ # Command for querying entities by association.
7
+ class FindMany < Cuprum::Command
8
+ PERMITTED_KEYWORDS = Set.new(%i[entities entity key keys]).freeze
9
+ private_constant :PERMITTED_KEYWORDS
10
+
11
+ # @!method call(**params)
12
+ # @overload call(key:)
13
+ # Finds the association values for the given key.
14
+ #
15
+ # @param key [Object] the primary or foreign key for querying the
16
+ # association.
17
+ #
18
+ # @return [Object, nil] the association value or nil, if the association
19
+ # is singular.
20
+ # @return [Array<Object>] the association values, if the association is
21
+ # plural.
22
+ #
23
+ # @overload call(keys:)
24
+ # Finds the association values for the given Array of keys.
25
+ # @return [Array<Object>] the association values.
26
+ #
27
+ # @param keys [Array<Object>] the primary or foreign keys for querying
28
+ # the association.
29
+ #
30
+ # @return [Array<Object>] the association values.
31
+ #
32
+ # @overload call(entity:)
33
+ # Finds the association values for the given entity.
34
+ #
35
+ # @param entity [Object] the base entity for querying the association.
36
+ #
37
+ # @return [Object, nil] the association value or nil, if the association
38
+ # is singular.
39
+ # @return [Array<Object>] the association values, if the association is
40
+ # plural.
41
+ #
42
+ # @overload call(entities:)
43
+ # Finds the association values for the given Array of entities.
44
+ #
45
+ # @param entity [Array<Object>] the base entities for querying the
46
+ # association.
47
+ #
48
+ # @return [Array<Object>] the association values.
49
+
50
+ # @param association [Cuprum::Collections::Association] the association to
51
+ # query.
52
+ # @param repository [Cuprum::Collections::Repository] the repository to
53
+ # query from.
54
+ # @param resource [Cuprum::Collections::Resource] the base resource for the
55
+ # association.
56
+ def initialize(association:, repository:, resource:)
57
+ super()
58
+
59
+ @association = association
60
+ @repository = repository
61
+ @resource = resource
62
+ end
63
+
64
+ # @return [Cuprum::Collections::Association] the association to query.
65
+ attr_reader :association
66
+
67
+ # @return [Cuprum::Collections::Repository] the repository to query from.
68
+ attr_reader :repository
69
+
70
+ # @return [Cuprum::Collections::Resource] the base resource for the
71
+ # association.
72
+ attr_reader :resource
73
+
74
+ private
75
+
76
+ def collection
77
+ repository.find_or_create(
78
+ name: tools.string_tools.pluralize(association.name),
79
+ qualified_name: association.qualified_name
80
+ )
81
+ end
82
+
83
+ def extract_keys(association, hsh)
84
+ return hsh[:key] if hsh.key?(:key)
85
+ return hsh[:keys] if hsh.key?(:keys)
86
+
87
+ values = hsh.fetch(:entity) { hsh[:entities] }
88
+
89
+ if values.is_a?(Array)
90
+ association.map_entities_to_keys(*values)
91
+ else
92
+ association.map_entities_to_keys(values).first
93
+ end
94
+ end
95
+
96
+ def handle_ambiguous_keys(hsh)
97
+ return if hsh.keys.size == 1
98
+
99
+ raise ArgumentError,
100
+ "ambiguous keywords #{hsh.each_key.map(&:inspect).join(', ')} " \
101
+ '- must provide exactly one parameter'
102
+ end
103
+
104
+ def handle_extra_keys(hsh)
105
+ return if hsh.keys.all? { |key| PERMITTED_KEYWORDS.include?(key) }
106
+
107
+ extra_keys = hsh.keys - PERMITTED_KEYWORDS.to_a
108
+
109
+ raise ArgumentError,
110
+ "invalid keywords #{extra_keys.map(&:inspect).join(', ')}"
111
+ end
112
+
113
+ def handle_missing_keys(hsh)
114
+ return unless hsh.empty?
115
+
116
+ raise ArgumentError, 'missing keyword :entity, :entities, :key, or :keys'
117
+ end
118
+
119
+ def perform_query(association:, expected_keys:, **)
120
+ query = association.build_keys_query(*expected_keys)
121
+ find_command = collection.find_matching
122
+
123
+ find_command.call(&query)
124
+ end
125
+
126
+ def process(**params) # rubocop:disable Metrics/MethodLength
127
+ association = @association.with_inverse(resource)
128
+ expected_keys, plural = resolve_keys(association, **params)
129
+ plural ||= association.plural?
130
+
131
+ return plural ? [] : nil if expected_keys.empty?
132
+
133
+ values = step do
134
+ perform_query(
135
+ association: association,
136
+ expected_keys: expected_keys,
137
+ plural: plural
138
+ )
139
+ end
140
+
141
+ plural ? values.to_a : values.first
142
+ end
143
+
144
+ def resolve_keys(association, **params)
145
+ handle_missing_keys(params)
146
+ handle_extra_keys(params)
147
+ handle_ambiguous_keys(params)
148
+
149
+ keys = extract_keys(association, params)
150
+ plural = keys.is_a?(Array)
151
+ keys = [keys] unless plural
152
+ keys = keys.compact.uniq
153
+
154
+ [keys, plural]
155
+ end
156
+
157
+ def tools
158
+ SleepingKingStudios::Tools::Toolbelt.instance
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/commands/associations'
4
+ require 'cuprum/collections/commands/associations/require_many'
5
+ require 'cuprum/collections/errors/not_found'
6
+
7
+ module Cuprum::Collections::Commands::Associations
8
+ # Command for querying required entities by association.
9
+ class RequireMany < Cuprum::Collections::Commands::Associations::FindMany
10
+ private
11
+
12
+ def find_missing_keys(entities:, expected_keys:)
13
+ expected_keys - map_entity_keys(entities: entities)
14
+ end
15
+
16
+ def map_entity_keys(entities:)
17
+ entities.map { |entity| entity[association.query_key_name] }
18
+ end
19
+
20
+ def missing_keys_error(missing_keys:, plural:)
21
+ attribute_value =
22
+ !plural && missing_keys.is_a?(Array) ? missing_keys.first : missing_keys
23
+
24
+ Cuprum::Collections::Errors::NotFound.new(
25
+ attribute_name: association.query_key_name,
26
+ attribute_value: attribute_value,
27
+ collection_name: association.name,
28
+ primary_key: association.primary_key_query?
29
+ )
30
+ end
31
+
32
+ def perform_query(association:, expected_keys:, plural:, **)
33
+ entities = step { super }
34
+ missing_keys = find_missing_keys(
35
+ entities: entities,
36
+ expected_keys: expected_keys
37
+ )
38
+
39
+ return success(entities) if missing_keys.empty?
40
+
41
+ missing_keys = missing_keys.first unless plural
42
+ error =
43
+ missing_keys_error(missing_keys: missing_keys, plural: plural)
44
+
45
+ failure(error)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/commands'
4
+
5
+ module Cuprum::Collections::Commands
6
+ # Namespace for commands that operate on entity associations.
7
+ module Associations
8
+ autoload :FindMany,
9
+ 'cuprum/collections/commands/associations/find_many'
10
+ autoload :RequireMany,
11
+ 'cuprum/collections/commands/associations/require_many'
12
+ end
13
+ end
@@ -92,7 +92,7 @@ module Cuprum::Collections::Commands
92
92
  private
93
93
 
94
94
  def error_params_for(attributes: nil, &block)
95
- { collection_name: collection.collection_name }.merge(
95
+ { collection_name: collection.name }.merge(
96
96
  if block_given?
97
97
  { query: collection.query.where(&block) }
98
98
  else
@@ -5,6 +5,7 @@ require 'cuprum/collections'
5
5
  module Cuprum::Collections
6
6
  # Namespace for abstract commands and collection-independent commands.
7
7
  module Commands
8
+ autoload :Associations, 'cuprum/collections/commands/associations'
8
9
  autoload :Create, 'cuprum/collections/commands/create'
9
10
  autoload :FindOneMatching, 'cuprum/collections/commands/find_one_matching'
10
11
  autoload :Update, 'cuprum/collections/commands/update'
@@ -14,7 +14,7 @@ module Cuprum::Collections::Errors
14
14
  ].freeze
15
15
  private_constant :PERMITTED_KEYWORDS
16
16
 
17
- # @overload initialize(attribute_name:, attribute_value:, collection_name:, primary_key: false) # rubocop:disable Layout/LineLength
17
+ # @overload initialize(attribute_name:, attribute_value:, collection_name:, primary_key: false)
18
18
  # @param attribute_name [String] The name of the queried attribute.
19
19
  # @param attribute_value [Object] The value of the queried attribute.
20
20
  # @param collection_name [String] The name of the collection.