cuprum-collections 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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.