rider-kick 0.0.12 → 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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +668 -27
  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 -44
  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 +61 -0
  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 +135 -0
  13. data/lib/generators/rider_kick/scaffold_generator.rb +377 -62
  14. data/lib/generators/rider_kick/scaffold_generator_builder_uploaders_spec.rb +159 -0
  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 +96 -0
  17. data/lib/generators/rider_kick/scaffold_generator_contracts_with_scope_spec.rb +83 -0
  18. data/lib/generators/rider_kick/scaffold_generator_engine_spec.rb +221 -0
  19. data/lib/generators/rider_kick/scaffold_generator_idempotent_spec.rb +84 -0
  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 +101 -0
  23. data/lib/generators/rider_kick/scaffold_generator_with_scope_spec.rb +76 -0
  24. data/lib/generators/rider_kick/structure_generator.rb +179 -35
  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 +20 -0
  28. data/lib/generators/rider_kick/structure_generator_success_spec.rb +64 -0
  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 +157 -51
  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 -16
  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 +12 -7
  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 +29 -0
  66. data/lib/generators/rider_kick/templates/domains/core/utils/request_methods.rb.tt +3 -2
  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 +9 -6
  79. data/lib/rider_kick/builders/abstract_active_record_entity_builder_spec.rb +596 -68
  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 +23 -15
  83. data/lib/rider_kick/entities/failure_details_spec.rb +22 -0
  84. data/lib/rider_kick/matchers/use_case_result.rb +1 -1
  85. data/lib/rider_kick/matchers/use_case_result_edge_spec.rb +28 -0
  86. data/lib/rider_kick/use_cases/abstract_use_case.rb +1 -1
  87. data/lib/rider_kick/use_cases/abstract_use_case_spec.rb +57 -0
  88. data/lib/rider_kick/version.rb +1 -1
  89. metadata +345 -52
  90. data/.rspec +0 -3
  91. data/.rubocop.yml +0 -1141
  92. data/CHANGELOG.md +0 -5
  93. data/Rakefile +0 -12
  94. data/lib/rider_kick/matchers/use_case_result_spec.rb +0 -64
@@ -1,116 +1,644 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
3
+ require 'dry-struct'
4
+ require 'active_support/core_ext/time'
5
+ require 'rider_kick/builders/abstract_active_record_entity_builder'
4
6
 
5
7
  RSpec.describe RiderKick::Builders::AbstractActiveRecordEntityBuilder do
6
- let(:entity_class) do
7
- Class.new(Dry::Struct) do
8
- attribute :id, Types::String
9
- attribute :name, Types::String
10
- attribute :related_entities, Types::Array.of(Types::Hash).optional.default([].freeze)
11
- attribute :other_entity, Types::Hash.optional.default(nil)
12
- end
8
+ # Create a test entity class using Dry::Struct
9
+ module Types
10
+ include Dry.Types()
13
11
  end
14
12
 
15
- let(:builder_class) do
16
- entity = entity_class
17
- Class.new(described_class) do
18
- acts_as_builder_for_entity(entity)
19
- end
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
20
18
  end
21
19
 
22
- let(:params) { instance_double('ActiveRecord::Base', attributes: { 'id' => '123e4567-e89b-12d3-a456-426614174000', 'name' => 'Sample Name' }) }
23
- let(:extra_param1) { 'extra1' }
24
- let(:extra_param2) { 'extra2' }
25
-
26
- subject(:builder) { builder_class.new(params, extra_param1, extra_param2) }
20
+ let(:test_entity_class) { test_entity_class }
27
21
 
28
- describe '#initialize' do
29
- it 'initializes with params and extra arguments' do
30
- expect { builder_class.new(params, extra_param1, extra_param2) }.not_to raise_error
31
- expect(builder.instance_variable_get(:@params)).to eq(params)
32
- expect(builder.instance_variable_get(:@args)).to eq([extra_param1, extra_param2])
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
+ })
33
32
  end
34
33
  end
35
34
 
36
35
  describe '.acts_as_builder_for_entity' do
37
- it 'sets entity class as a private method' do
38
- expect(builder.send(:entity_class)).to eq(entity_class)
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)
39
66
  end
40
67
  end
41
68
 
42
69
  describe '.has_many' do
43
- let(:related_entity_class) { double('RelatedEntityClass') }
44
- let(:relation_name) { :related_entities }
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
45
76
 
46
- before do
47
- builder_class.has_many relation_name, use: related_entity_class
48
- allow(params).to receive(relation_name).and_return([double('Relation')])
49
- allow(related_entity_class).to receive(:new).and_return(double(build: { id: 'abc123', name: 'Related' }))
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
50
82
  end
51
83
 
52
- it 'adds to has_many_builders' do
53
- expect(builder_class.has_many_builders).to include([relation_name, related_entity_class])
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)
54
87
  end
55
88
 
56
- it 'builds has_many relations' do
57
- result = builder.send(:attributes_for_has_many_relations)
58
- expect(result[relation_name]).to eq([{ id: 'abc123', name: 'Related' }])
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)
59
106
  end
60
107
  end
61
108
 
62
109
  describe '.belongs_to' do
63
- let(:other_entity_class) { double('OtherEntityClass') }
64
- let(:relation_name) { :other_entity }
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
65
155
 
66
- before do
67
- builder_class.belongs_to relation_name, use: other_entity_class
68
- allow(params).to receive(relation_name).and_return(double('Relation'))
69
- allow(other_entity_class).to receive(:new).and_return(double(build: { id: 'xyz789', name: 'Other' }))
156
+ it 'sets @params' do
157
+ builder = builder_class.new(mock_ar_model)
158
+ expect(builder.send(:params)).to eq(mock_ar_model)
70
159
  end
71
160
 
72
- it 'adds to belongs_to_builders' do
73
- expect(builder_class.belongs_to_builders).to include([relation_name, other_entity_class])
161
+ it 'sets @args' do
162
+ builder = builder_class.new(mock_ar_model, 'arg1', 'arg2')
163
+ expect(builder.send(:args)).to eq(['arg1', 'arg2'])
74
164
  end
75
165
 
76
- it 'builds belongs_to relation' do
77
- result = builder.send(:attributes_for_belongs_to_relations)
78
- expect(result[relation_name]).to eq({ id: 'xyz789', name: 'Other' })
166
+ it 'handles no additional args' do
167
+ builder = builder_class.new(mock_ar_model)
168
+ expect(builder.send(:args)).to eq([])
79
169
  end
80
170
  end
81
171
 
82
172
  describe '#build' do
83
- let(:attributes) { { id: '123e4567-e89b-12d3-a456-426614174000', name: 'Sample Name' } }
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
84
179
 
85
- it 'builds the entity with AR attributes' do
86
- allow(builder).to receive(:ar_attributes_for_entity).and_return(attributes)
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)
87
189
  entity = builder.build
88
- expect(entity.id).to eq('123e4567-e89b-12d3-a456-426614174000')
89
- expect(entity.name).to eq('Sample Name')
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
90
222
  end
91
223
 
92
- context 'when has_many and belongs_to relations exist' do
93
- let(:related_entity_class) { double('RelatedEntityClass') }
94
- let(:other_entity_class) { double('OtherEntityClass') }
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)
95
232
 
96
- before do
97
- builder_class.has_many :related_entities, use: related_entity_class
98
- builder_class.belongs_to :other_entity, use: other_entity_class
233
+ Class.new do
234
+ define_singleton_method(:decorator) { decorator }
235
+ end
236
+ end
99
237
 
100
- allow(params).to receive(:related_entities).and_return([double('Relation')])
101
- allow(params).to receive(:other_entity).and_return(double('Relation'))
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
102
244
 
103
- allow(related_entity_class).to receive(:new).and_return(double(build: { id: 'abc123', name: 'Related' }))
104
- allow(other_entity_class).to receive(:new).and_return(double(build: { id: 'xyz789', name: 'Other' }))
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)
105
249
  end
106
250
 
107
- it 'builds the entity with all attributes' do
108
- entity = builder.build
109
- expect(entity.id).to eq('123e4567-e89b-12d3-a456-426614174000')
110
- expect(entity.name).to eq('Sample Name')
111
- expect(entity.related_entities).to eq([{ id: 'abc123', name: 'Related' }])
112
- expect(entity.other_entity).to eq({ id: 'xyz789', name: 'Other' })
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
113
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)
114
642
  end
115
643
  end
116
644
  end