rider-kick 0.0.13 → 0.0.14

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +629 -25
  3. data/lib/generators/rider_kick/USAGE +2 -0
  4. data/lib/generators/rider_kick/base_generator.rb +190 -0
  5. data/lib/generators/rider_kick/clean_arch_generator.rb +235 -45
  6. data/lib/generators/rider_kick/clean_arch_generator_engine_spec.rb +359 -0
  7. data/lib/generators/rider_kick/clean_arch_generator_factory_bot_spec.rb +131 -0
  8. data/lib/generators/rider_kick/entity_type_mapping_spec.rb +22 -13
  9. data/lib/generators/rider_kick/errors.rb +42 -0
  10. data/lib/generators/rider_kick/factory_generator.rb +238 -0
  11. data/lib/generators/rider_kick/factory_generator_spec.rb +175 -0
  12. data/lib/generators/rider_kick/repositories_contract_spec.rb +95 -22
  13. data/lib/generators/rider_kick/scaffold_generator.rb +377 -62
  14. data/lib/generators/rider_kick/scaffold_generator_builder_uploaders_spec.rb +119 -14
  15. data/lib/generators/rider_kick/scaffold_generator_conditional_filtering_spec.rb +820 -0
  16. data/lib/generators/rider_kick/scaffold_generator_contracts_spec.rb +37 -10
  17. data/lib/generators/rider_kick/scaffold_generator_contracts_with_scope_spec.rb +40 -11
  18. data/lib/generators/rider_kick/scaffold_generator_engine_spec.rb +221 -0
  19. data/lib/generators/rider_kick/scaffold_generator_idempotent_spec.rb +38 -13
  20. data/lib/generators/rider_kick/scaffold_generator_list_spec_format_spec.rb +153 -0
  21. data/lib/generators/rider_kick/scaffold_generator_rspec_spec.rb +347 -0
  22. data/lib/generators/rider_kick/scaffold_generator_success_spec.rb +31 -12
  23. data/lib/generators/rider_kick/scaffold_generator_with_scope_spec.rb +32 -11
  24. data/lib/generators/rider_kick/structure_generator.rb +154 -43
  25. data/lib/generators/rider_kick/structure_generator_comprehensive_spec.rb +598 -0
  26. data/lib/generators/rider_kick/structure_generator_engine_spec.rb +279 -0
  27. data/lib/generators/rider_kick/structure_generator_spec.rb +3 -3
  28. data/lib/generators/rider_kick/structure_generator_success_spec.rb +33 -5
  29. data/lib/generators/rider_kick/structure_generator_unit_spec.rb +2202 -0
  30. data/lib/generators/rider_kick/templates/.rubocop.yml +5 -4
  31. data/lib/generators/rider_kick/templates/config/initializers/version.rb.tt +1 -1
  32. data/lib/generators/rider_kick/templates/db/migrate/20220613145533_init_database.rb +1 -1
  33. data/lib/generators/rider_kick/templates/db/structures/example.yaml.tt +140 -66
  34. data/lib/generators/rider_kick/templates/domains/core/builders/builder.rb.tt +36 -10
  35. data/lib/generators/rider_kick/templates/domains/core/builders/builder_spec.rb.tt +219 -0
  36. data/lib/generators/rider_kick/templates/domains/core/builders/error.rb.tt +2 -2
  37. data/lib/generators/rider_kick/templates/domains/core/builders/pagination.rb.tt +2 -2
  38. data/lib/generators/rider_kick/templates/domains/core/entities/entity.rb.tt +32 -14
  39. data/lib/generators/rider_kick/templates/domains/core/entities/error.rb.tt +1 -1
  40. data/lib/generators/rider_kick/templates/domains/core/entities/pagination.rb.tt +1 -1
  41. data/lib/generators/rider_kick/templates/domains/core/repositories/abstract_repository.rb.tt +4 -4
  42. data/lib/generators/rider_kick/templates/domains/core/repositories/create.rb.tt +2 -2
  43. data/lib/generators/rider_kick/templates/domains/core/repositories/create_spec.rb.tt +78 -0
  44. data/lib/generators/rider_kick/templates/domains/core/repositories/destroy.rb.tt +2 -2
  45. data/lib/generators/rider_kick/templates/domains/core/repositories/destroy_spec.rb.tt +88 -0
  46. data/lib/generators/rider_kick/templates/domains/core/repositories/fetch_by_id.rb.tt +3 -3
  47. data/lib/generators/rider_kick/templates/domains/core/repositories/fetch_by_id_spec.rb.tt +62 -0
  48. data/lib/generators/rider_kick/templates/domains/core/repositories/list.rb.tt +13 -8
  49. data/lib/generators/rider_kick/templates/domains/core/repositories/list_spec.rb.tt +190 -0
  50. data/lib/generators/rider_kick/templates/domains/core/repositories/update.rb.tt +4 -4
  51. data/lib/generators/rider_kick/templates/domains/core/repositories/update_spec.rb.tt +119 -0
  52. data/lib/generators/rider_kick/templates/domains/core/use_cases/contract/default.rb.tt +1 -1
  53. data/lib/generators/rider_kick/templates/domains/core/use_cases/contract/pagination.rb.tt +1 -1
  54. data/lib/generators/rider_kick/templates/domains/core/use_cases/create.rb.tt +3 -7
  55. data/lib/generators/rider_kick/templates/domains/core/use_cases/create_spec.rb.tt +71 -0
  56. data/lib/generators/rider_kick/templates/domains/core/use_cases/destroy.rb.tt +3 -7
  57. data/lib/generators/rider_kick/templates/domains/core/use_cases/destroy_spec.rb.tt +62 -0
  58. data/lib/generators/rider_kick/templates/domains/core/use_cases/fetch_by_id.rb.tt +3 -7
  59. data/lib/generators/rider_kick/templates/domains/core/use_cases/fetch_by_id_spec.rb.tt +62 -0
  60. data/lib/generators/rider_kick/templates/domains/core/use_cases/get_version.rb.tt +2 -2
  61. data/lib/generators/rider_kick/templates/domains/core/use_cases/list.rb.tt +3 -7
  62. data/lib/generators/rider_kick/templates/domains/core/use_cases/list_spec.rb.tt +64 -0
  63. data/lib/generators/rider_kick/templates/domains/core/use_cases/update.rb.tt +3 -7
  64. data/lib/generators/rider_kick/templates/domains/core/use_cases/update_spec.rb.tt +73 -0
  65. data/lib/generators/rider_kick/templates/domains/core/utils/abstract_utils.rb.tt +3 -3
  66. data/lib/generators/rider_kick/templates/domains/core/utils/request_methods.rb.tt +1 -1
  67. data/lib/generators/rider_kick/templates/env.development +1 -1
  68. data/lib/generators/rider_kick/templates/env.production +1 -1
  69. data/lib/generators/rider_kick/templates/env.test +1 -1
  70. data/lib/generators/rider_kick/templates/models/{application_record.rb → application_record.rb.tt} +3 -1
  71. data/lib/generators/rider_kick/templates/models/model_spec.rb.tt +68 -0
  72. data/lib/generators/rider_kick/templates/spec/factories/.gitkeep +19 -0
  73. data/lib/generators/rider_kick/templates/spec/factories/factory.rb.tt +8 -0
  74. data/lib/generators/rider_kick/templates/spec/rails_helper.rb +2 -0
  75. data/lib/generators/rider_kick/templates/spec/support/class_stubber.rb +148 -0
  76. data/lib/generators/rider_kick/templates/spec/support/factory_bot.rb +34 -0
  77. data/lib/generators/rider_kick/templates/spec/support/faker.rb +61 -0
  78. data/lib/rider-kick.rb +8 -6
  79. data/lib/rider_kick/builders/abstract_active_record_entity_builder_spec.rb +644 -0
  80. data/lib/rider_kick/configuration.rb +238 -0
  81. data/lib/rider_kick/configuration_engine_spec.rb +377 -0
  82. data/lib/rider_kick/entities/failure_details.rb +1 -1
  83. data/lib/rider_kick/entities/failure_details_spec.rb +1 -1
  84. data/lib/rider_kick/matchers/use_case_result.rb +1 -1
  85. data/lib/rider_kick/use_cases/abstract_use_case.rb +1 -1
  86. data/lib/rider_kick/version.rb +1 -1
  87. metadata +129 -8
  88. data/CHANGELOG.md +0 -5
@@ -0,0 +1,644 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-struct'
4
+ require 'active_support/core_ext/time'
5
+ require 'rider_kick/builders/abstract_active_record_entity_builder'
6
+
7
+ RSpec.describe RiderKick::Builders::AbstractActiveRecordEntityBuilder do
8
+ # Create a test entity class using Dry::Struct
9
+ module Types
10
+ include Dry.Types()
11
+ end
12
+
13
+ # Create entity class outside of let block so it's available in Class.new blocks
14
+ test_entity_class = Class.new(Dry::Struct) do
15
+ attribute :id, Types::String
16
+ attribute :name, Types::String.optional
17
+ attribute :email, Types::String.optional
18
+ end
19
+
20
+ let(:test_entity_class) { test_entity_class }
21
+
22
+ # Create a mock ActiveRecord model
23
+ let(:mock_ar_model) do
24
+ double('ActiveRecord::Base').tap do |model|
25
+ allow(model).to receive(:attributes).and_return({
26
+ 'id' => '123',
27
+ 'name' => 'Test User',
28
+ 'email' => 'test@example.com',
29
+ 'created_at' => Time.current,
30
+ 'updated_at' => Time.current
31
+ })
32
+ end
33
+ end
34
+
35
+ describe '.acts_as_builder_for_entity' do
36
+ let(:builder_class) do
37
+ entity_class = test_entity_class
38
+ Class.new(described_class) do
39
+ acts_as_builder_for_entity(entity_class)
40
+ end
41
+ end
42
+
43
+ it 'sets @has_many_builders and @belongs_to_builders to empty arrays' do
44
+ expect(builder_class.has_many_builders).to eq([])
45
+ expect(builder_class.belongs_to_builders).to eq([])
46
+ end
47
+
48
+ it 'defines singleton method has_many_builders' do
49
+ expect(builder_class).to respond_to(:has_many_builders)
50
+ expect(builder_class.has_many_builders).to be_an(Array)
51
+ end
52
+
53
+ it 'defines singleton method belongs_to_builders' do
54
+ expect(builder_class).to respond_to(:belongs_to_builders)
55
+ expect(builder_class.belongs_to_builders).to be_an(Array)
56
+ end
57
+
58
+ it 'defines instance method entity_class' do
59
+ builder = builder_class.new(mock_ar_model)
60
+ expect(builder.send(:entity_class)).to eq(test_entity_class)
61
+ end
62
+
63
+ it 'makes entity_class private' do
64
+ builder = builder_class.new(mock_ar_model)
65
+ expect { builder.entity_class }.to raise_error(NoMethodError)
66
+ end
67
+ end
68
+
69
+ describe '.has_many' do
70
+ let(:builder_class) do
71
+ entity_class = test_entity_class
72
+ Class.new(described_class) do
73
+ acts_as_builder_for_entity(entity_class)
74
+ end
75
+ end
76
+
77
+ let(:relation_builder_class) do
78
+ entity_class = test_entity_class
79
+ Class.new(described_class) do
80
+ acts_as_builder_for_entity(entity_class)
81
+ end
82
+ end
83
+
84
+ it 'adds to @has_many_builders array' do
85
+ builder_class.has_many(:comments, use: relation_builder_class)
86
+ expect(builder_class.has_many_builders.length).to eq(1)
87
+ end
88
+
89
+ it 'stores attribute_as, relation_name, and use' do
90
+ builder_class.has_many(:comments, attribute_as: :comment_list, use: relation_builder_class)
91
+ config = builder_class.has_many_builders.first
92
+ expect(config).to eq([:comment_list, :comments, relation_builder_class])
93
+ end
94
+
95
+ it 'uses relation_name when attribute_as is nil' do
96
+ builder_class.has_many(:comments, use: relation_builder_class)
97
+ config = builder_class.has_many_builders.first
98
+ expect(config[0]).to be_nil
99
+ expect(config[1]).to eq(:comments)
100
+ end
101
+
102
+ it 'handles multiple has_many declarations' do
103
+ builder_class.has_many(:comments, use: relation_builder_class)
104
+ builder_class.has_many(:tags, use: relation_builder_class)
105
+ expect(builder_class.has_many_builders.length).to eq(2)
106
+ end
107
+ end
108
+
109
+ describe '.belongs_to' do
110
+ let(:builder_class) do
111
+ entity_class = test_entity_class
112
+ Class.new(described_class) do
113
+ acts_as_builder_for_entity(entity_class)
114
+ end
115
+ end
116
+
117
+ let(:relation_builder_class) do
118
+ entity_class = test_entity_class
119
+ Class.new(described_class) do
120
+ acts_as_builder_for_entity(entity_class)
121
+ end
122
+ end
123
+
124
+ it 'adds to @belongs_to_builders array' do
125
+ builder_class.belongs_to(:user, use: relation_builder_class)
126
+ expect(builder_class.belongs_to_builders.length).to eq(1)
127
+ end
128
+
129
+ it 'stores attribute_as, relation_name, and use' do
130
+ builder_class.belongs_to(:user, attribute_as: :author, use: relation_builder_class)
131
+ config = builder_class.belongs_to_builders.first
132
+ expect(config).to eq([:author, :user, relation_builder_class])
133
+ end
134
+
135
+ it 'uses relation_name when attribute_as is nil' do
136
+ builder_class.belongs_to(:user, use: relation_builder_class)
137
+ config = builder_class.belongs_to_builders.first
138
+ expect(config[0]).to be_nil
139
+ expect(config[1]).to eq(:user)
140
+ end
141
+
142
+ it 'handles multiple belongs_to declarations' do
143
+ builder_class.belongs_to(:user, use: relation_builder_class)
144
+ builder_class.belongs_to(:account, use: relation_builder_class)
145
+ expect(builder_class.belongs_to_builders.length).to eq(2)
146
+ end
147
+ end
148
+
149
+ describe '#initialize' do
150
+ let(:builder_class) do
151
+ Class.new(described_class) do
152
+ acts_as_builder_for_entity(test_entity_class)
153
+ end
154
+ end
155
+
156
+ it 'sets @params' do
157
+ builder = builder_class.new(mock_ar_model)
158
+ expect(builder.send(:params)).to eq(mock_ar_model)
159
+ end
160
+
161
+ it 'sets @args' do
162
+ builder = builder_class.new(mock_ar_model, 'arg1', 'arg2')
163
+ expect(builder.send(:args)).to eq(['arg1', 'arg2'])
164
+ end
165
+
166
+ it 'handles no additional args' do
167
+ builder = builder_class.new(mock_ar_model)
168
+ expect(builder.send(:args)).to eq([])
169
+ end
170
+ end
171
+
172
+ describe '#build' do
173
+ let(:builder_class) do
174
+ entity_class = test_entity_class
175
+ Class.new(described_class) do
176
+ acts_as_builder_for_entity(entity_class)
177
+ end
178
+ end
179
+
180
+ it 'calls entity_class.new with all_attributes_for_entity' do
181
+ builder = builder_class.new(mock_ar_model)
182
+ attributes = builder.send(:all_attributes_for_entity)
183
+ expect(test_entity_class).to receive(:new).with(attributes)
184
+ builder.build
185
+ end
186
+
187
+ it 'returns entity instance' do
188
+ builder = builder_class.new(mock_ar_model)
189
+ entity = builder.build
190
+ expect(entity).to be_a(test_entity_class)
191
+ expect(entity.id).to eq('123')
192
+ end
193
+ end
194
+
195
+ describe '#entity_attribute_names' do
196
+ let(:builder_class) do
197
+ entity_class = test_entity_class
198
+ Class.new(described_class) do
199
+ acts_as_builder_for_entity(entity_class)
200
+ end
201
+ end
202
+
203
+ context 'with Dry::Struct schema' do
204
+ it 'detects Dry::Struct schema' do
205
+ builder = builder_class.new(mock_ar_model)
206
+ attribute_names = builder.send(:entity_attribute_names)
207
+ expect(attribute_names).to include(:id, :name, :email)
208
+ end
209
+
210
+ it 'returns symbol keys' do
211
+ builder = builder_class.new(mock_ar_model)
212
+ attribute_names = builder.send(:entity_attribute_names)
213
+ expect(attribute_names.all? { |k| k.is_a?(Symbol) }).to be true
214
+ end
215
+
216
+ it 'caches the result' do
217
+ builder = builder_class.new(mock_ar_model)
218
+ first_call = builder.send(:entity_attribute_names)
219
+ second_call = builder.send(:entity_attribute_names)
220
+ expect(first_call.object_id).to eq(second_call.object_id)
221
+ end
222
+ end
223
+
224
+ context 'with T::Struct schema (decorator)' do
225
+ let(:t_struct_entity_class) do
226
+ decorator_props = {
227
+ id: double('Prop', name: :id),
228
+ name: double('Prop', name: :name)
229
+ }
230
+ decorator = double('Decorator')
231
+ allow(decorator).to receive(:props).and_return(decorator_props)
232
+
233
+ Class.new do
234
+ define_singleton_method(:decorator) { decorator }
235
+ end
236
+ end
237
+
238
+ let(:t_struct_builder_class) do
239
+ entity_class = t_struct_entity_class
240
+ Class.new(described_class) do
241
+ acts_as_builder_for_entity(entity_class)
242
+ end
243
+ end
244
+
245
+ it 'detects T::Struct schema via decorator' do
246
+ builder = t_struct_builder_class.new(mock_ar_model)
247
+ attribute_names = builder.send(:entity_attribute_names)
248
+ expect(attribute_names).to include(:id, :name)
249
+ end
250
+
251
+ it 'extracts names from props with .name method' do
252
+ builder = t_struct_builder_class.new(mock_ar_model)
253
+ attribute_names = builder.send(:entity_attribute_names)
254
+ expect(attribute_names.all? { |k| k.is_a?(Symbol) }).to be true
255
+ end
256
+ end
257
+
258
+ context 'with unknown schema format' do
259
+ let(:unknown_entity_class) do
260
+ Class.new do
261
+ # No schema or decorator methods
262
+ end
263
+ end
264
+
265
+ let(:unknown_builder_class) do
266
+ entity_class = unknown_entity_class
267
+ Class.new(described_class) do
268
+ acts_as_builder_for_entity(entity_class)
269
+ end
270
+ end
271
+
272
+ it 'raises error for unknown schema format' do
273
+ builder = unknown_builder_class.new(mock_ar_model)
274
+ expect {
275
+ builder.send(:entity_attribute_names)
276
+ }.to raise_error('Cannot determine schema format')
277
+ end
278
+ end
279
+
280
+ context 'with keys that respond to .name' do
281
+ let(:name_key_entity_class) do
282
+ # Schema should return a hash where keys have .name method
283
+ key1 = double('Key', name: :id)
284
+ key2 = double('Key', name: :name)
285
+ schema_mock = double('Schema')
286
+ allow(schema_mock).to receive(:keys).and_return([key1, key2])
287
+
288
+ Class.new do
289
+ define_singleton_method(:schema) { schema_mock }
290
+ end
291
+ end
292
+
293
+ let(:name_key_builder_class) do
294
+ entity_class = name_key_entity_class
295
+ Class.new(described_class) do
296
+ acts_as_builder_for_entity(entity_class)
297
+ end
298
+ end
299
+
300
+ it 'extracts names from keys with .name method' do
301
+ builder = name_key_builder_class.new(mock_ar_model)
302
+ attribute_names = builder.send(:entity_attribute_names)
303
+ expect(attribute_names).to include(:id, :name)
304
+ end
305
+ end
306
+ end
307
+
308
+ describe '#params_attributes' do
309
+ let(:builder_class) do
310
+ entity_class = test_entity_class
311
+ Class.new(described_class) do
312
+ acts_as_builder_for_entity(entity_class)
313
+ end
314
+ end
315
+
316
+ it 'returns @params.attributes' do
317
+ builder = builder_class.new(mock_ar_model)
318
+ attributes = builder.send(:params_attributes)
319
+ expect(attributes).to eq(mock_ar_model.attributes)
320
+ end
321
+
322
+ it 'caches the result' do
323
+ builder = builder_class.new(mock_ar_model)
324
+ first_call = builder.send(:params_attributes)
325
+ second_call = builder.send(:params_attributes)
326
+ expect(first_call.object_id).to eq(second_call.object_id)
327
+ end
328
+ end
329
+
330
+ describe '#symbolized_params_attributes' do
331
+ let(:builder_class) do
332
+ entity_class = test_entity_class
333
+ Class.new(described_class) do
334
+ acts_as_builder_for_entity(entity_class)
335
+ end
336
+ end
337
+
338
+ it 'converts keys to symbols' do
339
+ builder = builder_class.new(mock_ar_model)
340
+ symbolized = builder.send(:symbolized_params_attributes)
341
+ expect(symbolized.keys.all? { |k| k.is_a?(Symbol) }).to be true
342
+ end
343
+
344
+ it 'returns hash with symbol keys' do
345
+ builder = builder_class.new(mock_ar_model)
346
+ symbolized = builder.send(:symbolized_params_attributes)
347
+ expect(symbolized).to be_a(Hash)
348
+ expect(symbolized[:id]).to eq('123')
349
+ expect(symbolized[:name]).to eq('Test User')
350
+ end
351
+
352
+ it 'caches the result' do
353
+ builder = builder_class.new(mock_ar_model)
354
+ first_call = builder.send(:symbolized_params_attributes)
355
+ second_call = builder.send(:symbolized_params_attributes)
356
+ expect(first_call.object_id).to eq(second_call.object_id)
357
+ end
358
+ end
359
+
360
+ describe '#ar_attributes_for_entity' do
361
+ let(:builder_class) do
362
+ entity_class = test_entity_class
363
+ Class.new(described_class) do
364
+ acts_as_builder_for_entity(entity_class)
365
+ end
366
+ end
367
+
368
+ it 'slices attributes based on entity_attribute_names' do
369
+ builder = builder_class.new(mock_ar_model)
370
+ ar_attributes = builder.send(:ar_attributes_for_entity)
371
+ expect(ar_attributes.keys).to all(satisfy { |k| [:id, :name, :email].include?(k) })
372
+ end
373
+
374
+ it 'returns only matching attributes' do
375
+ builder = builder_class.new(mock_ar_model)
376
+ ar_attributes = builder.send(:ar_attributes_for_entity)
377
+ expect(ar_attributes).to include(id: '123', name: 'Test User', email: 'test@example.com')
378
+ expect(ar_attributes).not_to have_key(:created_at)
379
+ expect(ar_attributes).not_to have_key(:updated_at)
380
+ end
381
+ end
382
+
383
+ describe '#attributes_for_belongs_to_relations' do
384
+ let(:related_entity_class) do
385
+ Class.new(Dry::Struct) do
386
+ attribute :id, Types::String
387
+ attribute :name, Types::String
388
+ end
389
+ end
390
+
391
+ let(:related_builder_class) do
392
+ entity_class = related_entity_class
393
+ Class.new(described_class) do
394
+ acts_as_builder_for_entity(entity_class)
395
+ end
396
+ end
397
+
398
+ let(:builder_class) do
399
+ entity_class = test_entity_class
400
+ related_builder = related_builder_class
401
+ Class.new(described_class) do
402
+ acts_as_builder_for_entity(entity_class)
403
+ belongs_to :user, use: related_builder
404
+ end
405
+ end
406
+
407
+ it 'maps belongs_to builders' do
408
+ related_model = double('User', attributes: { 'id' => '456', 'name' => 'Related User' })
409
+ ar_model = double('Article', attributes: { 'id' => '123' })
410
+ allow(ar_model).to receive(:user).and_return(related_model)
411
+
412
+ builder = builder_class.new(ar_model)
413
+ attributes = builder.send(:attributes_for_belongs_to_relations)
414
+ expect(attributes).to have_key(:user)
415
+ expect(attributes[:user]).to be_a(related_entity_class)
416
+ end
417
+
418
+ it 'handles nil relations' do
419
+ ar_model = double('Article', attributes: { 'id' => '123' })
420
+ allow(ar_model).to receive(:user).and_return(nil)
421
+
422
+ builder = builder_class.new(ar_model)
423
+ attributes = builder.send(:attributes_for_belongs_to_relations)
424
+ expect(attributes[:user]).to be_nil
425
+ end
426
+
427
+ it 'uses attribute_as when provided' do
428
+ entity_class = test_entity_class
429
+ related_builder = related_builder_class
430
+ builder_class_with_alias = Class.new(described_class) do
431
+ acts_as_builder_for_entity(entity_class)
432
+ belongs_to :user, attribute_as: :author, use: related_builder
433
+ end
434
+
435
+ related_model = double('User', attributes: { 'id' => '456', 'name' => 'Related User' })
436
+ ar_model = double('Article', attributes: { 'id' => '123' })
437
+ allow(ar_model).to receive(:user).and_return(related_model)
438
+
439
+ builder = builder_class_with_alias.new(ar_model)
440
+ attributes = builder.send(:attributes_for_belongs_to_relations)
441
+ expect(attributes).to have_key(:author)
442
+ expect(attributes).not_to have_key(:user)
443
+ end
444
+
445
+ it 'uses relation_name when attribute_as is nil' do
446
+ related_model = double('User', attributes: { 'id' => '456', 'name' => 'Related User' })
447
+ ar_model = double('Article', attributes: { 'id' => '123' })
448
+ allow(ar_model).to receive(:user).and_return(related_model)
449
+
450
+ builder = builder_class.new(ar_model)
451
+ attributes = builder.send(:attributes_for_belongs_to_relations)
452
+ expect(attributes).to have_key(:user)
453
+ end
454
+ end
455
+
456
+ describe '#attributes_for_has_many_relations' do
457
+ let(:comment_entity_class) do
458
+ Class.new(Dry::Struct) do
459
+ attribute :id, Types::String
460
+ attribute :content, Types::String
461
+ end
462
+ end
463
+
464
+ let(:comment_builder_class) do
465
+ entity_class = comment_entity_class
466
+ Class.new(described_class) do
467
+ acts_as_builder_for_entity(entity_class)
468
+ end
469
+ end
470
+
471
+ let(:builder_class) do
472
+ entity_class = test_entity_class
473
+ comment_builder = comment_builder_class
474
+ Class.new(described_class) do
475
+ acts_as_builder_for_entity(entity_class)
476
+ has_many :comments, use: comment_builder
477
+ end
478
+ end
479
+
480
+ it 'maps has_many builders' do
481
+ comment1 = double('Comment', attributes: { 'id' => '1', 'content' => 'Comment 1' })
482
+ comment2 = double('Comment', attributes: { 'id' => '2', 'content' => 'Comment 2' })
483
+ ar_model = double('Article', attributes: { 'id' => '123' })
484
+ allow(ar_model).to receive(:comments).and_return([comment1, comment2])
485
+
486
+ builder = builder_class.new(ar_model)
487
+ attributes = builder.send(:attributes_for_has_many_relations)
488
+ expect(attributes).to have_key(:comments)
489
+ expect(attributes[:comments]).to be_an(Array)
490
+ expect(attributes[:comments].length).to eq(2)
491
+ expect(attributes[:comments].all? { |c| c.is_a?(comment_entity_class) }).to be true
492
+ end
493
+
494
+ it 'builds multiple relations' do
495
+ comment1 = double('Comment', attributes: { 'id' => '1', 'content' => 'Comment 1' })
496
+ comment2 = double('Comment', attributes: { 'id' => '2', 'content' => 'Comment 2' })
497
+ ar_model = double('Article', attributes: { 'id' => '123' })
498
+ allow(ar_model).to receive(:comments).and_return([comment1, comment2])
499
+
500
+ builder = builder_class.new(ar_model)
501
+ attributes = builder.send(:attributes_for_has_many_relations)
502
+ expect(attributes[:comments].first.id).to eq('1')
503
+ expect(attributes[:comments].last.id).to eq('2')
504
+ end
505
+
506
+ it 'uses attribute_as when provided' do
507
+ entity_class = test_entity_class
508
+ comment_builder = comment_builder_class
509
+ builder_class_with_alias = Class.new(described_class) do
510
+ acts_as_builder_for_entity(entity_class)
511
+ has_many :comments, attribute_as: :comment_list, use: comment_builder
512
+ end
513
+
514
+ comment1 = double('Comment', attributes: { 'id' => '1', 'content' => 'Comment 1' })
515
+ ar_model = double('Article', attributes: { 'id' => '123' })
516
+ allow(ar_model).to receive(:comments).and_return([comment1])
517
+
518
+ builder = builder_class_with_alias.new(ar_model)
519
+ attributes = builder.send(:attributes_for_has_many_relations)
520
+ expect(attributes).to have_key(:comment_list)
521
+ expect(attributes).not_to have_key(:comments)
522
+ end
523
+
524
+ it 'uses relation_name when attribute_as is nil' do
525
+ comment1 = double('Comment', attributes: { 'id' => '1', 'content' => 'Comment 1' })
526
+ ar_model = double('Article', attributes: { 'id' => '123' })
527
+ allow(ar_model).to receive(:comments).and_return([comment1])
528
+
529
+ builder = builder_class.new(ar_model)
530
+ attributes = builder.send(:attributes_for_has_many_relations)
531
+ expect(attributes).to have_key(:comments)
532
+ end
533
+ end
534
+
535
+ describe '#attributes_for_entity' do
536
+ let(:builder_class) do
537
+ entity_class = test_entity_class
538
+ Class.new(described_class) do
539
+ acts_as_builder_for_entity(entity_class)
540
+ end
541
+ end
542
+
543
+ it 'returns empty hash (default implementation)' do
544
+ builder = builder_class.new(mock_ar_model)
545
+ attributes = builder.send(:attributes_for_entity)
546
+ expect(attributes).to eq({})
547
+ end
548
+ end
549
+
550
+ describe '#all_attributes_for_entity' do
551
+ let(:related_entity_class) do
552
+ Class.new(Dry::Struct) do
553
+ attribute :id, Types::String
554
+ attribute :name, Types::String
555
+ end
556
+ end
557
+
558
+ let(:comment_entity_class) do
559
+ Class.new(Dry::Struct) do
560
+ attribute :id, Types::String
561
+ attribute :content, Types::String
562
+ end
563
+ end
564
+
565
+ let(:related_builder_class) do
566
+ entity_class = related_entity_class
567
+ Class.new(described_class) do
568
+ acts_as_builder_for_entity(entity_class)
569
+ end
570
+ end
571
+
572
+ let(:comment_builder_class) do
573
+ entity_class = comment_entity_class
574
+ Class.new(described_class) do
575
+ acts_as_builder_for_entity(entity_class)
576
+ end
577
+ end
578
+
579
+ let(:builder_class) do
580
+ entity_class = test_entity_class
581
+ related_builder = related_builder_class
582
+ comment_builder = comment_builder_class
583
+ Class.new(described_class) do
584
+ acts_as_builder_for_entity(entity_class)
585
+ belongs_to :user, use: related_builder
586
+ has_many :comments, use: comment_builder
587
+ end
588
+ end
589
+
590
+ it 'merges all attribute sources' do
591
+ related_model = double('User', attributes: { 'id' => '456', 'name' => 'Related User' })
592
+ comment1 = double('Comment', attributes: { 'id' => '1', 'content' => 'Comment 1' })
593
+ ar_model = double('Article', attributes: { 'id' => '123', 'name' => 'Article', 'email' => 'article@example.com' })
594
+ allow(ar_model).to receive(:user).and_return(related_model)
595
+ allow(ar_model).to receive(:comments).and_return([comment1])
596
+
597
+ builder = builder_class.new(ar_model)
598
+ all_attributes = builder.send(:all_attributes_for_entity)
599
+
600
+ expect(all_attributes).to have_key(:id)
601
+ expect(all_attributes).to have_key(:name)
602
+ expect(all_attributes).to have_key(:email)
603
+ expect(all_attributes).to have_key(:user)
604
+ expect(all_attributes).to have_key(:comments)
605
+ end
606
+
607
+ it 'merges in correct order: ar_attributes → belongs_to → has_many → custom' do
608
+ related_model = double('User', attributes: { 'id' => '456', 'name' => 'Related User' })
609
+ comment1 = double('Comment', attributes: { 'id' => '1', 'content' => 'Comment 1' })
610
+ ar_model = double('Article', attributes: { 'id' => '123', 'name' => 'Article', 'email' => 'article@example.com' })
611
+ allow(ar_model).to receive(:user).and_return(related_model)
612
+ allow(ar_model).to receive(:comments).and_return([comment1])
613
+
614
+ # Create custom builder class that inherits from builder_class
615
+ # Need to ensure it has the same configuration
616
+ entity_class = test_entity_class
617
+ related_builder = related_builder_class
618
+ comment_builder = comment_builder_class
619
+ custom_builder_class = Class.new(described_class) do
620
+ acts_as_builder_for_entity(entity_class)
621
+ belongs_to :user, use: related_builder
622
+ has_many :comments, use: comment_builder
623
+
624
+ define_method(:attributes_for_entity) do
625
+ { custom_field: 'custom_value' }
626
+ end
627
+ end
628
+
629
+ builder = custom_builder_class.new(ar_model)
630
+ all_attributes = builder.send(:all_attributes_for_entity)
631
+
632
+ # Custom attributes should be last (override previous)
633
+ expect(all_attributes[:custom_field]).to eq('custom_value')
634
+ # AR attributes should be present
635
+ expect(all_attributes[:id]).to eq('123')
636
+ expect(all_attributes[:name]).to eq('Article')
637
+ expect(all_attributes[:email]).to eq('article@example.com')
638
+ # Relations should be present
639
+ expect(all_attributes[:user]).to be_a(related_entity_class)
640
+ expect(all_attributes[:comments]).to be_an(Array)
641
+ expect(all_attributes[:comments].first).to be_a(comment_entity_class)
642
+ end
643
+ end
644
+ end