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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/DEVELOPMENT.md +2 -2
- data/README.md +13 -11
- data/lib/cuprum/collections/association.rb +256 -0
- data/lib/cuprum/collections/associations/belongs_to.rb +32 -0
- data/lib/cuprum/collections/associations/has_many.rb +23 -0
- data/lib/cuprum/collections/associations/has_one.rb +23 -0
- data/lib/cuprum/collections/associations.rb +10 -0
- data/lib/cuprum/collections/basic/collection.rb +39 -74
- data/lib/cuprum/collections/basic/commands/find_many.rb +1 -1
- data/lib/cuprum/collections/basic/commands/find_matching.rb +1 -1
- data/lib/cuprum/collections/basic/repository.rb +9 -33
- data/lib/cuprum/collections/basic.rb +1 -0
- data/lib/cuprum/collections/collection.rb +154 -0
- data/lib/cuprum/collections/commands/associations/find_many.rb +161 -0
- data/lib/cuprum/collections/commands/associations/require_many.rb +48 -0
- data/lib/cuprum/collections/commands/associations.rb +13 -0
- data/lib/cuprum/collections/commands/find_one_matching.rb +1 -1
- data/lib/cuprum/collections/commands.rb +1 -0
- data/lib/cuprum/collections/errors/abstract_find_error.rb +1 -1
- data/lib/cuprum/collections/relation.rb +401 -0
- data/lib/cuprum/collections/repository.rb +71 -4
- data/lib/cuprum/collections/resource.rb +65 -0
- data/lib/cuprum/collections/rspec/contracts/association_contracts.rb +2137 -0
- data/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb +484 -0
- data/lib/cuprum/collections/rspec/contracts/basic.rb +11 -0
- data/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +429 -0
- data/lib/cuprum/collections/rspec/contracts/command_contracts.rb +1462 -0
- data/lib/cuprum/collections/rspec/contracts/query_contracts.rb +1093 -0
- data/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +1381 -0
- data/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +605 -0
- data/lib/cuprum/collections/rspec/contracts.rb +23 -0
- data/lib/cuprum/collections/rspec/fixtures.rb +85 -82
- data/lib/cuprum/collections/rspec.rb +4 -1
- data/lib/cuprum/collections/version.rb +1 -1
- data/lib/cuprum/collections.rb +9 -4
- metadata +23 -19
- data/lib/cuprum/collections/base.rb +0 -11
- data/lib/cuprum/collections/basic/rspec/command_contract.rb +0 -392
- data/lib/cuprum/collections/rspec/assign_one_command_contract.rb +0 -168
- data/lib/cuprum/collections/rspec/build_one_command_contract.rb +0 -93
- data/lib/cuprum/collections/rspec/collection_contract.rb +0 -190
- data/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +0 -108
- data/lib/cuprum/collections/rspec/find_many_command_contract.rb +0 -407
- data/lib/cuprum/collections/rspec/find_matching_command_contract.rb +0 -194
- data/lib/cuprum/collections/rspec/find_one_command_contract.rb +0 -157
- data/lib/cuprum/collections/rspec/insert_one_command_contract.rb +0 -84
- data/lib/cuprum/collections/rspec/query_builder_contract.rb +0 -92
- data/lib/cuprum/collections/rspec/query_contract.rb +0 -650
- data/lib/cuprum/collections/rspec/querying_contract.rb +0 -298
- data/lib/cuprum/collections/rspec/repository_contract.rb +0 -235
- data/lib/cuprum/collections/rspec/update_one_command_contract.rb +0 -80
- 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
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
)
|
23
|
+
qualified_name =
|
24
|
+
Cuprum::Collections::Relation::Disambiguation
|
25
|
+
.resolve_parameters(parameters, name: :collection_name)
|
26
|
+
.fetch(:qualified_name)
|
37
27
|
|
38
|
-
|
28
|
+
data ||= @data.fetch(qualified_name, [])
|
39
29
|
|
40
|
-
|
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.
|
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)
|
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.
|