cuprum-collections 0.4.0 → 0.5.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -0
  3. data/README.md +5 -5
  4. data/lib/cuprum/collections/association.rb +9 -28
  5. data/lib/cuprum/collections/associations/belongs_to.rb +1 -8
  6. data/lib/cuprum/collections/associations/has_many.rb +1 -10
  7. data/lib/cuprum/collections/associations/has_one.rb +1 -10
  8. data/lib/cuprum/collections/basic/collection.rb +56 -49
  9. data/lib/cuprum/collections/basic/command.rb +22 -88
  10. data/lib/cuprum/collections/basic/commands/assign_one.rb +2 -6
  11. data/lib/cuprum/collections/basic/commands/build_one.rb +1 -4
  12. data/lib/cuprum/collections/basic/commands/destroy_one.rb +4 -8
  13. data/lib/cuprum/collections/basic/commands/find_many.rb +4 -24
  14. data/lib/cuprum/collections/basic/commands/find_matching.rb +5 -21
  15. data/lib/cuprum/collections/basic/commands/find_one.rb +3 -20
  16. data/lib/cuprum/collections/basic/commands/insert_one.rb +3 -6
  17. data/lib/cuprum/collections/basic/commands/update_one.rb +3 -6
  18. data/lib/cuprum/collections/basic/commands/validate_one.rb +13 -18
  19. data/lib/cuprum/collections/basic/query.rb +26 -40
  20. data/lib/cuprum/collections/basic/repository.rb +4 -3
  21. data/lib/cuprum/collections/basic/scopes/all_scope.rb +25 -0
  22. data/lib/cuprum/collections/basic/scopes/base.rb +32 -0
  23. data/lib/cuprum/collections/basic/scopes/builder.rb +39 -0
  24. data/lib/cuprum/collections/basic/scopes/conjunction_scope.rb +20 -0
  25. data/lib/cuprum/collections/basic/scopes/criteria_scope.rb +62 -0
  26. data/lib/cuprum/collections/basic/scopes/disjunction_scope.rb +20 -0
  27. data/lib/cuprum/collections/basic/scopes/none_scope.rb +33 -0
  28. data/lib/cuprum/collections/basic/scopes.rb +23 -0
  29. data/lib/cuprum/collections/basic.rb +1 -0
  30. data/lib/cuprum/collections/collection.rb +24 -82
  31. data/lib/cuprum/collections/collection_command.rb +116 -0
  32. data/lib/cuprum/collections/commands/abstract_find_many.rb +11 -21
  33. data/lib/cuprum/collections/commands/abstract_find_matching.rb +43 -24
  34. data/lib/cuprum/collections/commands/abstract_find_one.rb +7 -10
  35. data/lib/cuprum/collections/commands/associations/find_many.rb +3 -8
  36. data/lib/cuprum/collections/commands/associations/require_many.rb +5 -5
  37. data/lib/cuprum/collections/commands/create.rb +3 -3
  38. data/lib/cuprum/collections/commands/find_one_matching.rb +6 -6
  39. data/lib/cuprum/collections/commands/query_command.rb +19 -0
  40. data/lib/cuprum/collections/commands/update.rb +3 -3
  41. data/lib/cuprum/collections/commands/upsert.rb +10 -10
  42. data/lib/cuprum/collections/commands.rb +1 -0
  43. data/lib/cuprum/collections/constraints/ordering.rb +2 -2
  44. data/lib/cuprum/collections/errors/abstract_find_error.rb +25 -42
  45. data/lib/cuprum/collections/errors/extra_attributes.rb +3 -3
  46. data/lib/cuprum/collections/errors/failed_validation.rb +2 -2
  47. data/lib/cuprum/collections/errors/invalid_parameters.rb +2 -2
  48. data/lib/cuprum/collections/errors/invalid_query.rb +10 -16
  49. data/lib/cuprum/collections/errors/missing_default_contract.rb +1 -1
  50. data/lib/cuprum/collections/errors/unknown_operator.rb +1 -1
  51. data/lib/cuprum/collections/queries.rb +31 -0
  52. data/lib/cuprum/collections/query.rb +50 -62
  53. data/lib/cuprum/collections/relation.rb +5 -383
  54. data/lib/cuprum/collections/relations/cardinality.rb +66 -0
  55. data/lib/cuprum/collections/relations/options.rb +18 -0
  56. data/lib/cuprum/collections/relations/parameters.rb +217 -0
  57. data/lib/cuprum/collections/relations/primary_keys.rb +23 -0
  58. data/lib/cuprum/collections/relations/scope.rb +65 -0
  59. data/lib/cuprum/collections/relations.rb +14 -0
  60. data/lib/cuprum/collections/repository.rb +5 -5
  61. data/lib/cuprum/collections/resource.rb +10 -41
  62. data/lib/cuprum/collections/rspec/contracts/association_contracts.rb +80 -90
  63. data/lib/cuprum/collections/rspec/contracts/collection_contracts.rb +69 -111
  64. data/lib/cuprum/collections/rspec/contracts/command_contracts.rb +42 -1335
  65. data/lib/cuprum/collections/rspec/contracts/query_contracts.rb +352 -531
  66. data/lib/cuprum/collections/rspec/contracts/relation_contracts.rb +74 -191
  67. data/lib/cuprum/collections/rspec/contracts/repository_contracts.rb +13 -13
  68. data/lib/cuprum/collections/rspec/contracts/scope_contracts.rb +1029 -0
  69. data/lib/cuprum/collections/rspec/contracts/scopes/builder_contracts.rb +856 -0
  70. data/lib/cuprum/collections/rspec/contracts/scopes/composition_contracts.rb +1430 -0
  71. data/lib/cuprum/collections/rspec/contracts/scopes/criteria_contracts.rb +2217 -0
  72. data/lib/cuprum/collections/rspec/contracts/scopes/logical_contracts.rb +297 -0
  73. data/lib/cuprum/collections/rspec/contracts/scopes.rb +13 -0
  74. data/lib/cuprum/collections/rspec/contracts.rb +2 -0
  75. data/lib/cuprum/collections/rspec/deferred/association_examples.rb +2098 -0
  76. data/lib/cuprum/collections/rspec/deferred/collection_examples.rb +338 -0
  77. data/lib/cuprum/collections/rspec/deferred/command_examples.rb +160 -0
  78. data/lib/cuprum/collections/rspec/deferred/commands/assign_one_examples.rb +178 -0
  79. data/lib/cuprum/collections/rspec/deferred/commands/build_one_examples.rb +94 -0
  80. data/lib/cuprum/collections/rspec/deferred/commands/destroy_one_examples.rb +118 -0
  81. data/lib/cuprum/collections/rspec/deferred/commands/find_many_examples.rb +307 -0
  82. data/lib/cuprum/collections/rspec/deferred/commands/find_matching_examples.rb +143 -0
  83. data/lib/cuprum/collections/rspec/deferred/commands/find_one_examples.rb +116 -0
  84. data/lib/cuprum/collections/rspec/deferred/commands/insert_one_examples.rb +103 -0
  85. data/lib/cuprum/collections/rspec/deferred/commands/update_one_examples.rb +99 -0
  86. data/lib/cuprum/collections/rspec/deferred/commands/validate_one_examples.rb +117 -0
  87. data/lib/cuprum/collections/rspec/deferred/commands.rb +8 -0
  88. data/lib/cuprum/collections/rspec/deferred/relation_examples.rb +1437 -0
  89. data/lib/cuprum/collections/rspec/deferred/resource_examples.rb +26 -0
  90. data/lib/cuprum/collections/rspec/deferred.rb +8 -0
  91. data/lib/cuprum/collections/scope.rb +29 -0
  92. data/lib/cuprum/collections/scopes/all.rb +51 -0
  93. data/lib/cuprum/collections/scopes/all_scope.rb +18 -0
  94. data/lib/cuprum/collections/scopes/base.rb +79 -0
  95. data/lib/cuprum/collections/scopes/builder.rb +39 -0
  96. data/lib/cuprum/collections/scopes/building.rb +221 -0
  97. data/lib/cuprum/collections/scopes/composition.rb +162 -0
  98. data/lib/cuprum/collections/scopes/conjunction.rb +44 -0
  99. data/lib/cuprum/collections/scopes/conjunction_scope.rb +12 -0
  100. data/lib/cuprum/collections/scopes/container.rb +65 -0
  101. data/lib/cuprum/collections/scopes/criteria/parser.rb +241 -0
  102. data/lib/cuprum/collections/scopes/criteria.rb +206 -0
  103. data/lib/cuprum/collections/scopes/criteria_scope.rb +12 -0
  104. data/lib/cuprum/collections/scopes/disjunction.rb +45 -0
  105. data/lib/cuprum/collections/scopes/disjunction_scope.rb +12 -0
  106. data/lib/cuprum/collections/scopes/none.rb +62 -0
  107. data/lib/cuprum/collections/scopes/none_scope.rb +18 -0
  108. data/lib/cuprum/collections/scopes.rb +23 -0
  109. data/lib/cuprum/collections/version.rb +2 -2
  110. data/lib/cuprum/collections.rb +14 -9
  111. metadata +61 -15
  112. data/lib/cuprum/collections/basic/query_builder.rb +0 -69
  113. data/lib/cuprum/collections/command.rb +0 -26
  114. data/lib/cuprum/collections/queries/parse.rb +0 -22
  115. data/lib/cuprum/collections/queries/parse_block.rb +0 -206
  116. data/lib/cuprum/collections/queries/parse_strategy.rb +0 -91
  117. data/lib/cuprum/collections/query_builder.rb +0 -61
  118. data/lib/cuprum/collections/rspec/contracts/basic/command_contracts.rb +0 -484
@@ -3,399 +3,21 @@
3
3
  require 'set'
4
4
 
5
5
  require 'cuprum/collections'
6
+ require 'cuprum/collections/relations/options'
7
+ require 'cuprum/collections/relations/parameters'
6
8
 
7
9
  module Cuprum::Collections
8
10
  # Abstract class representing a group or view of entities.
9
11
  class Relation
10
- # Methods for resolving a singular or plural relation.
11
- module Cardinality
12
- # @return [Boolean] true if the relation is plural; otherwise false.
13
- def plural?
14
- @plural
15
- end
12
+ include Cuprum::Collections::Relations::Options
13
+ include Cuprum::Collections::Relations::Parameters
16
14
 
17
- # @return [Boolean] true if the relation is singular; otherwise false.
18
- def singular?
19
- !@plural
20
- end
21
-
22
- private
23
-
24
- def resolve_plurality(**params) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
25
- if params.key?(:plural) && !params[:plural].nil?
26
- if params.key?(:singular) && !params[:singular].nil?
27
- message =
28
- 'ambiguous cardinality: initialized with parameters ' \
29
- "plural: #{params[:plural].inspect} and singular: " \
30
- "#{params[:singular].inspect}"
31
-
32
- raise ArgumentError, message
33
- end
34
-
35
- validate_cardinality(params[:plural], as: 'plural')
36
-
37
- return params[:plural]
38
- end
39
-
40
- if params.key?(:singular) && !params[:singular].nil?
41
- validate_cardinality(params[:singular], as: 'singular')
42
-
43
- return !params[:singular]
44
- end
45
-
46
- true
47
- end
48
-
49
- def validate_cardinality(value, as:)
50
- return if value == true || value == false # rubocop:disable Style/MultipleComparison
51
-
52
- raise ArgumentError, "#{as} must be true or false"
53
- end
54
- end
55
-
56
- # Methods for disambiguating parameters with multiple keywords.
57
- module Disambiguation
58
- class << self
59
- # Helper method for resolving an ambiguous keyword.
60
- #
61
- # @param params [Hash] the original method keywords.
62
- # @param key [Symbol] the original key to resolve.
63
- # @param alternatives [Symbol, Array<Symbol>] the additional keywords.
64
- #
65
- # @return [Hash] the disambiguated keywords.
66
- def disambiguate_keyword(params, key, *alternatives) # rubocop:disable Metrics/MethodLength
67
- params = params.dup
68
- values = keyword_values(params, key, *alternatives)
69
-
70
- return params if values.empty?
71
-
72
- if values.size == 1
73
- match, value = values.first
74
-
75
- unless match == key
76
- tools.core_tools.deprecate(
77
- "#{match.inspect} keyword",
78
- message: "Use #{key.inspect} instead"
79
- )
80
- end
81
-
82
- return params.merge(key => value)
83
- end
84
-
85
- raise ArgumentError, ambiguous_keywords_error(values)
86
- end
87
-
88
- # Helper method for resolving a Relation's required parameters.
89
- #
90
- # The returned Hash will define the :entity_class, :singular_name,
91
- # :name, and :qualified_name keys.
92
- #
93
- # @param params [Hash] the parameters to resolve.
94
- # @param ambiguous [Hash{Symbol => Symbol, Array<Symbol>}] ambiguous
95
- # keywords to resolve. Each key-value pair is passed to
96
- # .disambiguate_keyword before the parameters are resolved.
97
- #
98
- # @return [Hash] the resolved parameters.
99
- #
100
- # @see .disambiguate_keyword
101
- # @see Cuprum::Collections::Relation::Parameters.resolve_parameters
102
- def resolve_parameters(params, **ambiguous)
103
- params = ambiguous.reduce(params) do |hsh, (key, alternatives)|
104
- disambiguate_keyword(hsh, key, *alternatives)
105
- end
106
-
107
- Cuprum::Collections::Relation::Parameters.resolve_parameters(params)
108
- end
109
-
110
- private
111
-
112
- def ambiguous_keywords_error(values)
113
- expected, _ = values.first
114
- formatted =
115
- values
116
- .map { |key, value| "#{key}: #{value.inspect}" }
117
- .join(', ')
118
-
119
- "ambiguous parameter #{expected}: initialized with parameters " \
120
- "#{formatted}"
121
- end
122
-
123
- def keyword_values(keywords, *keys)
124
- keys
125
- .map { |key| [key, keywords.delete(key)] }
126
- .reject { |_, value| value.nil? } # rubocop:disable Style/CollectionCompact
127
- end
128
-
129
- def tools
130
- SleepingKingStudios::Tools::Toolbelt.instance
131
- end
132
- end
133
-
134
- # (see Cuprum::Collections::Relation::Disambiguation.disambiguate_keyword)
135
- def disambiguate_keyword(params, key, *alternatives)
136
- Disambiguation.disambiguate_keyword(params, key, *alternatives)
137
- end
138
-
139
- # (see Cuprum::Collections::Relation::Disambiguation.resolve_parameters)
140
- def resolve_parameters(params, **ambiguous)
141
- Disambiguation.resolve_parameters(params, **ambiguous)
142
- end
143
- end
144
-
145
- # Methods for resolving a relations's naming and entity class from options.
146
- module Parameters # rubocop:disable Metrics/ModuleLength
147
- PARAMETER_KEYS = %i[entity_class name qualified_name].freeze
148
- private_constant :PARAMETER_KEYS
149
-
150
- class << self
151
- # @overload resolve_parameters(entity_class: nil, singular_name: nil, name: nil, qualified_name: nil)
152
- # Helper method for resolving a Relation's required parameters.
153
- #
154
- # The returned Hash will define the :entity_class, :singular_name,
155
- # :name, and :qualified_name keys.
156
- #
157
- # @param entity_class [Class, String] the class of entity represented
158
- # by the relation.
159
- # @param singular_name [String] the name of an entity in the relation.
160
- # @param name [String] the name of the relation.
161
- # @param qualified_name [String] a scoped name for the relation.
162
- #
163
- # @return [Hash] the resolved parameters.
164
- def resolve_parameters(params) # rubocop:disable Metrics/MethodLength
165
- validate_parameters(**params)
166
-
167
- entity_class = entity_class_from(**params)
168
- class_name = entity_class_name(entity_class)
169
- name = relation_name_from(**params, class_name: class_name)
170
- plural_name = plural_name_from(**params, name: name)
171
- qualified_name = qualified_name_from(**params, class_name: class_name)
172
- singular_name = singular_name_from(**params, name: name)
173
-
174
- {
175
- entity_class: entity_class,
176
- name: name,
177
- plural_name: plural_name,
178
- qualified_name: qualified_name,
179
- singular_name: singular_name
180
- }
181
- end
182
-
183
- private
184
-
185
- def classify(raw)
186
- raw
187
- .then { |str| tools.string_tools.singularize(str).to_s }
188
- .split('/')
189
- .map { |str| tools.string_tools.camelize(str) }
190
- .join('::')
191
- end
192
-
193
- def entity_class_from(**params)
194
- if has_key?(params, :entity_class)
195
- entity_class = params[:entity_class]
196
-
197
- return entity_class.is_a?(Class) ? entity_class : entity_class.to_s
198
- end
199
-
200
- if has_key?(params, :qualified_name)
201
- return classify(params[:qualified_name])
202
- end
203
-
204
- classify(params[:name])
205
- end
206
-
207
- def entity_class_name(entity_class, scoped: true)
208
- (entity_class.is_a?(Class) ? entity_class.name : entity_class)
209
- .split('::')
210
- .map { |str| tools.string_tools.underscore(str) }
211
- .then { |ary| scoped ? ary.join('/') : ary.last }
212
- end
213
-
214
- def has_key?(params, key) # rubocop:disable Naming/PredicateName
215
- return false unless params.key?(key)
216
-
217
- !params[key].nil?
218
- end
219
-
220
- def plural_name_from(name:, **parameters)
221
- if parameters.key?(:plural_name) && !parameters[:plural_name].nil?
222
- return validate_parameter(
223
- parameters[:plural_name],
224
- as: 'plural name'
225
- )
226
- end
227
-
228
- tools.string_tools.pluralize(name)
229
- end
230
-
231
- def qualified_name_from(class_name:, **params)
232
- if has_key?(params, :qualified_name)
233
- return params[:qualified_name].to_s
234
- end
235
-
236
- tools.string_tools.pluralize(class_name)
237
- end
238
-
239
- def relation_name_from(class_name:, **params)
240
- return params[:name].to_s if has_key?(params, :name)
241
-
242
- tools.string_tools.pluralize(class_name.split('/').last)
243
- end
244
-
245
- def singular_name_from(name:, **parameters)
246
- if parameters.key?(:singular_name) && !parameters[:singular_name].nil?
247
- return validate_parameter(
248
- parameters[:singular_name],
249
- as: 'singular name'
250
- )
251
- end
252
-
253
- tools.string_tools.singularize(name)
254
- end
255
-
256
- def tools
257
- SleepingKingStudios::Tools::Toolbelt.instance
258
- end
259
-
260
- def validate_entity_class(value)
261
- return if value.is_a?(Class)
262
-
263
- if value.nil? || value.is_a?(String) || value.is_a?(Symbol)
264
- tools.assertions.validate_name(value, as: 'entity class')
265
-
266
- return
267
- end
268
-
269
- raise ArgumentError,
270
- 'entity class is not a Class, a String or a Symbol'
271
- end
272
-
273
- def validate_parameter(value, as:)
274
- tools.assertions.validate_name(value, as: as)
275
-
276
- value.to_s
277
- end
278
-
279
- def validate_parameter_keys(params)
280
- return if PARAMETER_KEYS.any? { |key| has_key?(params, key) }
281
-
282
- raise ArgumentError, "name or entity class can't be blank"
283
- end
284
-
285
- def validate_parameters(**params) # rubocop:disable Metrics/MethodLength
286
- validate_parameter_keys(params)
287
-
288
- if has_key?(params, :entity_class)
289
- validate_entity_class(params[:entity_class])
290
- end
291
-
292
- if has_key?(params, :name)
293
- validate_parameter(params[:name], as: 'name')
294
- end
295
-
296
- if has_key?(params, :plural_name)
297
- validate_parameter(params[:plural_name], as: 'plural name')
298
- end
299
-
300
- if has_key?(params, :qualified_name)
301
- validate_parameter(params[:qualified_name], as: 'qualified name')
302
- end
303
-
304
- if has_key?(params, :singular_name) # rubocop:disable Style/GuardClause
305
- validate_parameter(params[:singular_name], as: 'singular name')
306
- end
307
- end
308
- end
309
-
310
- # @return [String] the name of the relation.
311
- attr_reader :name
312
-
313
- # @return [String] the pluralized name of the relation.
314
- attr_reader :plural_name
315
-
316
- # @return [String] a scoped name for the relation.
317
- attr_reader :qualified_name
318
-
319
- # @return [String] the name of an entity in the relation.
320
- attr_reader :singular_name
321
-
322
- # @return [Class] the class of entity represented by the relation.
323
- def entity_class
324
- return @entity_class if @entity_class.is_a?(Class)
325
-
326
- @entity_class = Object.const_get(@entity_class)
327
- end
328
-
329
- # (see Cuprum::Collections::Relation::Parameters.resolve_parameters)
330
- def resolve_parameters(parameters)
331
- Parameters.resolve_parameters(parameters)
332
- end
333
- end
334
-
335
- # Methods for specifying a relation's primary key.
336
- module PrimaryKeys
337
- # @return [String] the name of the primary key attribute. Defaults to
338
- # 'id'.
339
- def primary_key_name
340
- @primary_key_name ||= options.fetch(:primary_key_name, 'id').to_s
341
- end
342
-
343
- # @return [Class, Stannum::Constraint] the type of the primary key
344
- # attribute. Defaults to Integer.
345
- def primary_key_type
346
- @primary_key_type ||=
347
- options
348
- .fetch(:primary_key_type, Integer)
349
- .then { |obj| obj.is_a?(String) ? Object.const_get(obj) : obj }
350
- end
351
- end
352
-
353
- IGNORED_PARAMETERS = %i[
354
- entity_class
355
- name
356
- qualified_name
357
- singular_name
358
- ].freeze
359
- private_constant :IGNORED_PARAMETERS
360
-
361
- include Cuprum::Collections::Relation::Parameters
362
-
363
- # @overload initialize(entity_class: nil, name: nil, qualified_name: nil, singular_name: nil, **options)
15
+ # @!method initialize(entity_class: nil, name: nil, qualified_name: nil, singular_name: nil, **options)
364
16
  # @param entity_class [Class, String] the class of entity represented by
365
17
  # the relation.
366
18
  # @param name [String] the name of the relation.
367
19
  # @param qualified_name [String] a scoped name for the relation.
368
20
  # @param singular_name [String] the name of an entity in the relation.
369
21
  # @param options [Hash] additional options for the relation.
370
- def initialize(**parameters)
371
- relation_params = resolve_parameters(parameters)
372
-
373
- @entity_class = relation_params[:entity_class]
374
- @name = relation_params[:name]
375
- @plural_name = relation_params[:plural_name]
376
- @qualified_name = relation_params[:qualified_name]
377
- @singular_name = relation_params[:singular_name]
378
-
379
- @options = ignore_parameters(**parameters)
380
- end
381
-
382
- # @return [Hash] additional options for the relation.
383
- attr_reader :options
384
-
385
- private
386
-
387
- def ignore_parameters(**parameters)
388
- parameters
389
- .reject { |key, _| ignored_parameters.include?(key) }
390
- .to_h
391
- end
392
-
393
- def ignored_parameters
394
- @ignored_parameters ||= Set.new(IGNORED_PARAMETERS)
395
- end
396
-
397
- def tools
398
- SleepingKingStudios::Tools::Toolbelt.instance
399
- end
400
22
  end
401
23
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/relations'
4
+
5
+ module Cuprum::Collections::Relations
6
+ # Methods for resolving a singular or plural relation.
7
+ module Cardinality
8
+ IGNORED_PARAMETERS = %i[
9
+ plural
10
+ singular
11
+ ].freeze
12
+ private_constant :IGNORED_PARAMETERS
13
+
14
+ # @overload initialize(plural: true, **)
15
+ # @param plural [Boolean] if true, the resource represents a plural
16
+ # resource. Defaults to true. Can also be specified as :singular.
17
+ def initialize(**parameters)
18
+ super(**parameters.except(*IGNORED_PARAMETERS))
19
+
20
+ @plural = resolve_plurality(**parameters)
21
+ end
22
+
23
+ # @return [Boolean] true if the relation is plural; otherwise false.
24
+ def plural?
25
+ @plural
26
+ end
27
+
28
+ # @return [Boolean] true if the relation is singular; otherwise false.
29
+ def singular?
30
+ !@plural
31
+ end
32
+
33
+ private
34
+
35
+ def resolve_plurality(**params) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
36
+ if params.key?(:plural) && !params[:plural].nil?
37
+ if params.key?(:singular) && !params[:singular].nil?
38
+ message =
39
+ 'ambiguous cardinality: initialized with parameters ' \
40
+ "plural: #{params[:plural].inspect} and singular: " \
41
+ "#{params[:singular].inspect}"
42
+
43
+ raise ArgumentError, message
44
+ end
45
+
46
+ validate_cardinality(params[:plural], as: 'plural')
47
+
48
+ return params[:plural]
49
+ end
50
+
51
+ if params.key?(:singular) && !params[:singular].nil?
52
+ validate_cardinality(params[:singular], as: 'singular')
53
+
54
+ return !params[:singular]
55
+ end
56
+
57
+ true
58
+ end
59
+
60
+ def validate_cardinality(value, as:)
61
+ return if value == true || value == false # rubocop:disable Style/MultipleComparison
62
+
63
+ raise ArgumentError, "#{as} must be true or false"
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/relations'
4
+
5
+ module Cuprum::Collections::Relations
6
+ # Methods for storing arbitrary options for a relation.
7
+ module Options
8
+ # @param options [Hash] additional options for the relation.
9
+ def initialize(**options)
10
+ super()
11
+
12
+ @options = options
13
+ end
14
+
15
+ # @return [Hash] additional options for the relation.
16
+ attr_reader :options
17
+ end
18
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/relations'
4
+
5
+ module Cuprum::Collections::Relations
6
+ # Methods for resolving a relations's naming and entity class from options.
7
+ module Parameters # rubocop:disable Metrics/ModuleLength
8
+ IGNORED_PARAMETERS = %i[
9
+ entity_class
10
+ name
11
+ qualified_name
12
+ singular_name
13
+ ].freeze
14
+ private_constant :IGNORED_PARAMETERS
15
+
16
+ PARAMETER_KEYS = %i[entity_class name qualified_name].freeze
17
+ private_constant :PARAMETER_KEYS
18
+
19
+ class << self # rubocop:disable Metrics/ClassLength
20
+ # @overload resolve_parameters(entity_class: nil, singular_name: nil, name: nil, qualified_name: nil)
21
+ # Helper method for resolving a Relation's required parameters.
22
+ #
23
+ # The returned Hash will define the :entity_class, :singular_name,
24
+ # :name, and :qualified_name keys.
25
+ #
26
+ # @param entity_class [Class, String] the class of entity represented
27
+ # by the relation.
28
+ # @param singular_name [String] the name of an entity in the relation.
29
+ # @param name [String] the name of the relation.
30
+ # @param qualified_name [String] a scoped name for the relation.
31
+ #
32
+ # @return [Hash] the resolved parameters.
33
+ def resolve_parameters(params) # rubocop:disable Metrics/MethodLength
34
+ validate_parameters(**params)
35
+
36
+ entity_class = entity_class_from(**params)
37
+ class_name = entity_class_name(entity_class)
38
+ name = relation_name_from(**params, class_name:)
39
+ plural_name = plural_name_from(**params, name:)
40
+ qualified_name = qualified_name_from(**params, class_name:)
41
+ singular_name = singular_name_from(**params, name:)
42
+
43
+ {
44
+ entity_class:,
45
+ name:,
46
+ plural_name:,
47
+ qualified_name:,
48
+ singular_name:
49
+ }
50
+ end
51
+
52
+ private
53
+
54
+ def classify(raw)
55
+ raw
56
+ .then { |str| tools.string_tools.singularize(str).to_s }
57
+ .split('/')
58
+ .map { |str| tools.string_tools.camelize(str) }
59
+ .join('::')
60
+ end
61
+
62
+ def entity_class_from(**params)
63
+ if has_key?(params, :entity_class)
64
+ entity_class = params[:entity_class]
65
+
66
+ return entity_class.is_a?(Class) ? entity_class : entity_class.to_s
67
+ end
68
+
69
+ if has_key?(params, :qualified_name)
70
+ return classify(params[:qualified_name])
71
+ end
72
+
73
+ classify(params[:name])
74
+ end
75
+
76
+ def entity_class_name(entity_class, scoped: true)
77
+ (entity_class.is_a?(Class) ? entity_class.name : entity_class)
78
+ .split('::')
79
+ .map { |str| tools.string_tools.underscore(str) }
80
+ .then { |ary| scoped ? ary.join('/') : ary.last }
81
+ end
82
+
83
+ def has_key?(params, key) # rubocop:disable Naming/PredicatePrefix
84
+ return false unless params.key?(key)
85
+
86
+ !params[key].nil?
87
+ end
88
+
89
+ def plural_name_from(name:, **parameters)
90
+ if parameters.key?(:plural_name) && !parameters[:plural_name].nil?
91
+ return validate_parameter(
92
+ parameters[:plural_name],
93
+ as: 'plural name'
94
+ )
95
+ end
96
+
97
+ tools.string_tools.pluralize(name)
98
+ end
99
+
100
+ def qualified_name_from(class_name:, **params)
101
+ return params[:qualified_name].to_s if has_key?(params, :qualified_name)
102
+
103
+ tools.string_tools.pluralize(class_name)
104
+ end
105
+
106
+ def relation_name_from(class_name:, **params)
107
+ return params[:name].to_s if has_key?(params, :name)
108
+
109
+ tools.string_tools.pluralize(class_name.split('/').last)
110
+ end
111
+
112
+ def singular_name_from(name:, **parameters)
113
+ if parameters.key?(:singular_name) && !parameters[:singular_name].nil?
114
+ return validate_parameter(
115
+ parameters[:singular_name],
116
+ as: 'singular name'
117
+ )
118
+ end
119
+
120
+ tools.string_tools.singularize(name)
121
+ end
122
+
123
+ def tools
124
+ SleepingKingStudios::Tools::Toolbelt.instance
125
+ end
126
+
127
+ def validate_entity_class(value)
128
+ return if value.is_a?(Class)
129
+
130
+ if value.nil? || value.is_a?(String) || value.is_a?(Symbol)
131
+ tools.assertions.validate_name(value, as: 'entity class')
132
+
133
+ return
134
+ end
135
+
136
+ raise ArgumentError,
137
+ 'entity class is not a Class, a String or a Symbol'
138
+ end
139
+
140
+ def validate_parameter(value, as:)
141
+ tools.assertions.validate_name(value, as:)
142
+
143
+ value.to_s
144
+ end
145
+
146
+ def validate_parameter_keys(params)
147
+ return if PARAMETER_KEYS.any? { |key| has_key?(params, key) }
148
+
149
+ raise ArgumentError, "name or entity class can't be blank"
150
+ end
151
+
152
+ def validate_parameters(**params) # rubocop:disable Metrics/MethodLength
153
+ validate_parameter_keys(params)
154
+
155
+ if has_key?(params, :entity_class)
156
+ validate_entity_class(params[:entity_class])
157
+ end
158
+
159
+ validate_parameter(params[:name], as: 'name') if has_key?(params, :name)
160
+
161
+ if has_key?(params, :plural_name)
162
+ validate_parameter(params[:plural_name], as: 'plural name')
163
+ end
164
+
165
+ if has_key?(params, :qualified_name)
166
+ validate_parameter(params[:qualified_name], as: 'qualified name')
167
+ end
168
+
169
+ if has_key?(params, :singular_name) # rubocop:disable Style/GuardClause
170
+ validate_parameter(params[:singular_name], as: 'singular name')
171
+ end
172
+ end
173
+ end
174
+
175
+ # @overload initialize(entity_class: nil, name: nil, qualified_name: nil, singular_name: nil, **)
176
+ # @param entity_class [Class, String] the class of entity represented by
177
+ # the relation.
178
+ # @param name [String] the name of the relation.
179
+ # @param qualified_name [String] a scoped name for the relation.
180
+ # @param singular_name [String] the name of an entity in the relation.
181
+ def initialize(**parameters)
182
+ super(**parameters.except(*IGNORED_PARAMETERS))
183
+
184
+ relation_params = resolve_parameters(parameters)
185
+
186
+ @entity_class = relation_params[:entity_class]
187
+ @name = relation_params[:name]
188
+ @plural_name = relation_params[:plural_name]
189
+ @qualified_name = relation_params[:qualified_name]
190
+ @singular_name = relation_params[:singular_name]
191
+ end
192
+
193
+ # @return [String] the name of the relation.
194
+ attr_reader :name
195
+
196
+ # @return [String] the pluralized name of the relation.
197
+ attr_reader :plural_name
198
+
199
+ # @return [String] a scoped name for the relation.
200
+ attr_reader :qualified_name
201
+
202
+ # @return [String] the name of an entity in the relation.
203
+ attr_reader :singular_name
204
+
205
+ # @return [Class] the class of entity represented by the relation.
206
+ def entity_class
207
+ return @entity_class if @entity_class.is_a?(Class)
208
+
209
+ @entity_class = Object.const_get(@entity_class)
210
+ end
211
+
212
+ # (see Cuprum::Collections::Relations::Parameters.resolve_parameters)
213
+ def resolve_parameters(parameters)
214
+ Parameters.resolve_parameters(parameters)
215
+ end
216
+ end
217
+ end