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,598 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+ require 'yaml'
7
+ require 'debug'
8
+ require 'generators/rider_kick/structure_generator'
9
+
10
+ # Define Column struct at top level to avoid dynamic constant assignment warning
11
+ TestColumn = Struct.new(:name, :type, :sql_type, :null, :default, :precision, :scale, :limit) unless defined?(TestColumn)
12
+
13
+ RSpec.describe 'rider_kick:structure generator (comprehensive)' do
14
+ let(:klass) { RiderKick::Structure }
15
+
16
+ def create_test_model(module_name: 'Models', class_name: 'Article', columns: [])
17
+ Object.send(:remove_const, :Models) if Object.const_defined?(:Models)
18
+
19
+ unless Object.const_defined?(module_name.to_sym)
20
+ Object.const_set(module_name.to_sym, Module.new)
21
+ end
22
+
23
+ model_class = Class.new do
24
+ define_singleton_method(:columns) { columns }
25
+
26
+ define_singleton_method(:columns_hash) do
27
+ columns.to_h { |c| [c.name.to_s, Struct.new(:type).new(c.type)] }
28
+ end
29
+
30
+ define_singleton_method(:column_names) do
31
+ columns.map { |c| c.name.to_s }
32
+ end
33
+ end
34
+
35
+ module_obj = Object.const_get(module_name)
36
+ module_obj.const_set(class_name.to_sym, model_class)
37
+
38
+ "#{module_name}::#{class_name}".constantize
39
+ end
40
+
41
+ def create_test_columns
42
+ [
43
+ TestColumn.new('id', :uuid, 'uuid', false),
44
+ TestColumn.new('account_id', :uuid, 'uuid', true),
45
+ TestColumn.new('title', :string, 'character varying', true),
46
+ TestColumn.new('body', :text, 'text', true),
47
+ TestColumn.new('published_at', :datetime, 'timestamp without time zone', true),
48
+ TestColumn.new('user_id', :uuid, 'uuid', true),
49
+ TestColumn.new('images', :text, 'text', true),
50
+ TestColumn.new('created_at', :datetime, 'timestamp without time zone', false),
51
+ TestColumn.new('updated_at', :datetime, 'timestamp without time zone', false)
52
+ ]
53
+ end
54
+
55
+ before do
56
+ RiderKick.configuration.engine_name = nil
57
+ end
58
+
59
+ describe 'structure file generation' do
60
+ it 'generates complete structure file with all sections' do
61
+ Dir.mktmpdir do |dir|
62
+ Dir.chdir(dir) do
63
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
64
+ FileUtils.mkdir_p('app/models/models')
65
+
66
+ create_test_model(class_name: 'Article', columns: create_test_columns)
67
+
68
+ instance = klass.new(['Models::Article', 'actor:owner', 'uploaders:images,assets', 'search_able:title,body', 'resource_owner_id:account_id', 'resource_owner:account'])
69
+ allow(instance).to receive(:options).and_return({ engine: nil })
70
+ instance.generate_use_case
71
+
72
+ expect(File).to exist('db/structures/articles_structure.yaml')
73
+
74
+ yaml_content = File.read('db/structures/articles_structure.yaml')
75
+ parsed = YAML.load(yaml_content)
76
+
77
+ # Verify basic structure
78
+ expect(parsed['model']).to eq('Models::Article')
79
+ expect(parsed['resource_name']).to eq('articles')
80
+ expect(parsed['actor']).to eq('owner')
81
+ expect(parsed['resource_owner_id']).to eq('account_id')
82
+ expect(parsed['resource_owner']).to eq('account')
83
+ end
84
+ end
85
+ end
86
+
87
+ it 'generates correct fields section' do
88
+ Dir.mktmpdir do |dir|
89
+ Dir.chdir(dir) do
90
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
91
+ FileUtils.mkdir_p('app/models/models')
92
+
93
+ create_test_model(class_name: 'Article', columns: create_test_columns)
94
+
95
+ instance = klass.new(['Models::Article', 'actor:owner', 'uploaders:images', 'resource_owner:account', 'resource_owner_id:account_id'])
96
+ allow(instance).to receive(:options).and_return({ engine: nil })
97
+ instance.generate_use_case
98
+
99
+ yaml_content = File.read('db/structures/articles_structure.yaml')
100
+
101
+ # Check that fields include database columns and uploaders
102
+ expect(yaml_content).to include('- account_id')
103
+ expect(yaml_content).to include('- title')
104
+ expect(yaml_content).to include('- body')
105
+ expect(yaml_content).to include('- images')
106
+ end
107
+ end
108
+ end
109
+
110
+ it 'generates uploaders section with inline empty array format' do
111
+ Dir.mktmpdir do |dir|
112
+ Dir.chdir(dir) do
113
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
114
+ FileUtils.mkdir_p('app/models/models')
115
+
116
+ create_test_model(class_name: 'Article', columns: create_test_columns)
117
+
118
+ # Test with empty uploaders
119
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner:account', 'resource_owner_id:account_id'])
120
+ allow(instance).to receive(:options).and_return({ engine: nil })
121
+ instance.generate_use_case
122
+
123
+ yaml_content = File.read('db/structures/articles_structure.yaml')
124
+
125
+ # Should be inline format: uploaders: []
126
+ expect(yaml_content).to match(/^uploaders:\s*\[\]/m)
127
+ # Should not be multi-line (key on one line, [] on next line)
128
+ expect(yaml_content).not_to match(/^uploaders:\s*\n\s*\[\]/m)
129
+ end
130
+ end
131
+ end
132
+
133
+ it 'generates uploaders section with uploader definitions' do
134
+ Dir.mktmpdir do |dir|
135
+ Dir.chdir(dir) do
136
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
137
+ FileUtils.mkdir_p('app/models/models')
138
+
139
+ create_test_model(class_name: 'Article', columns: create_test_columns)
140
+
141
+ instance = klass.new(['Models::Article', 'actor:owner', 'uploaders:images,assets', 'resource_owner:account', 'resource_owner_id:account_id'])
142
+ allow(instance).to receive(:options).and_return({ engine: nil })
143
+ instance.generate_use_case
144
+
145
+ yaml_content = File.read('db/structures/articles_structure.yaml')
146
+ parsed = YAML.load(yaml_content)
147
+
148
+ expect(parsed['uploaders']).to be_an(Array)
149
+ expect(parsed['uploaders'].length).to eq(2)
150
+ expect(parsed['uploaders']).to include({ 'name' => 'images', 'type' => 'multiple' })
151
+ expect(parsed['uploaders']).to include({ 'name' => 'assets', 'type' => 'multiple' })
152
+ end
153
+ end
154
+ end
155
+
156
+ it 'generates search_able section with inline empty array format' do
157
+ Dir.mktmpdir do |dir|
158
+ Dir.chdir(dir) do
159
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
160
+ FileUtils.mkdir_p('app/models/models')
161
+
162
+ create_test_model(class_name: 'Article', columns: create_test_columns)
163
+
164
+ # Test with empty search_able
165
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner:account', 'resource_owner_id:account_id'])
166
+ allow(instance).to receive(:options).and_return({ engine: nil })
167
+ instance.generate_use_case
168
+
169
+ yaml_content = File.read('db/structures/articles_structure.yaml')
170
+
171
+ # Should be inline format: search_able: []
172
+ expect(yaml_content).to match(/^search_able:\s*\[\]/m)
173
+ end
174
+ end
175
+ end
176
+
177
+ it 'generates search_able section with searchable fields' do
178
+ Dir.mktmpdir do |dir|
179
+ Dir.chdir(dir) do
180
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
181
+ FileUtils.mkdir_p('app/models/models')
182
+
183
+ create_test_model(class_name: 'Article', columns: create_test_columns)
184
+
185
+ instance = klass.new(['Models::Article', 'actor:owner', 'search_able:title,body', 'resource_owner:account', 'resource_owner_id:account_id'])
186
+ allow(instance).to receive(:options).and_return({ engine: nil })
187
+ instance.generate_use_case
188
+
189
+ yaml_content = File.read('db/structures/articles_structure.yaml')
190
+ parsed = YAML.load(yaml_content)
191
+
192
+ expect(parsed['search_able']).to be_an(Array)
193
+ expect(parsed['search_able']).to include('title')
194
+ expect(parsed['search_able']).to include('body')
195
+ end
196
+ end
197
+ end
198
+
199
+ it 'generates schema section with all column metadata' do
200
+ Dir.mktmpdir do |dir|
201
+ Dir.chdir(dir) do
202
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
203
+ FileUtils.mkdir_p('app/models/models')
204
+
205
+ create_test_model(class_name: 'Article', columns: create_test_columns)
206
+
207
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner:account', 'resource_owner_id:account_id'])
208
+ allow(instance).to receive(:options).and_return({ engine: nil })
209
+ instance.generate_use_case
210
+
211
+ yaml_content = File.read('db/structures/articles_structure.yaml')
212
+ parsed = YAML.load(yaml_content)
213
+
214
+ expect(parsed['schema']).to be_a(Hash)
215
+ expect(parsed['schema']['columns']).to be_an(Array)
216
+ expect(parsed['schema']['columns'].length).to eq(9) # All columns including id, timestamps
217
+
218
+ # Check column metadata
219
+ id_column = parsed['schema']['columns'].find { |c| c['name'] == 'id' }
220
+ expect(id_column['type']).to eq('uuid')
221
+ # null can be nil, false, or 'null' string depending on column definition
222
+ expect([false, nil, 'null']).to include(id_column['null'])
223
+ end
224
+ end
225
+ end
226
+
227
+ it 'generates schema foreign_keys with inline empty array format' do
228
+ Dir.mktmpdir do |dir|
229
+ Dir.chdir(dir) do
230
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
231
+ FileUtils.mkdir_p('app/models/models')
232
+
233
+ create_test_model(class_name: 'Article', columns: create_test_columns)
234
+
235
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner:account', 'resource_owner_id:account_id'])
236
+ allow(instance).to receive(:options).and_return({ engine: nil })
237
+ instance.generate_use_case
238
+
239
+ yaml_content = File.read('db/structures/articles_structure.yaml')
240
+
241
+ # Should be inline format: foreign_keys: []
242
+ expect(yaml_content).to match(/^\s+foreign_keys:\s*\[\]/m)
243
+ end
244
+ end
245
+ end
246
+
247
+ it 'generates schema indexes with inline empty array format' do
248
+ Dir.mktmpdir do |dir|
249
+ Dir.chdir(dir) do
250
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
251
+ FileUtils.mkdir_p('app/models/models')
252
+
253
+ create_test_model(class_name: 'Article', columns: create_test_columns)
254
+
255
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner:account', 'resource_owner_id:account_id'])
256
+ allow(instance).to receive(:options).and_return({ engine: nil })
257
+ instance.generate_use_case
258
+
259
+ yaml_content = File.read('db/structures/articles_structure.yaml')
260
+
261
+ # Should be inline format: indexes: []
262
+ expect(yaml_content).to match(/^\s+indexes:\s*\[\]/m)
263
+ end
264
+ end
265
+ end
266
+
267
+ it 'generates schema enums with empty object format' do
268
+ Dir.mktmpdir do |dir|
269
+ Dir.chdir(dir) do
270
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
271
+ FileUtils.mkdir_p('app/models/models')
272
+
273
+ create_test_model(class_name: 'Article', columns: create_test_columns)
274
+
275
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner:account', 'resource_owner_id:account_id'])
276
+ allow(instance).to receive(:options).and_return({ engine: nil })
277
+ instance.generate_use_case
278
+
279
+ yaml_content = File.read('db/structures/articles_structure.yaml')
280
+ parsed = YAML.load(yaml_content)
281
+
282
+ expect(parsed['schema']['enums']).to eq({})
283
+ end
284
+ end
285
+ end
286
+
287
+ it 'generates controllers section with list_fields, show_fields, and form_fields' do
288
+ Dir.mktmpdir do |dir|
289
+ Dir.chdir(dir) do
290
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
291
+ FileUtils.mkdir_p('app/models/models')
292
+
293
+ create_test_model(class_name: 'Article', columns: create_test_columns)
294
+
295
+ instance = klass.new(['Models::Article', 'actor:owner', 'uploaders:images', 'resource_owner:account', 'resource_owner_id:account_id'])
296
+ allow(instance).to receive(:options).and_return({ engine: nil })
297
+ instance.generate_use_case
298
+
299
+ yaml_content = File.read('db/structures/articles_structure.yaml')
300
+ parsed = YAML.load(yaml_content)
301
+
302
+ expect(parsed['controllers']).to be_a(Hash)
303
+ expect(parsed['controllers']['list_fields']).to be_an(Array)
304
+ expect(parsed['controllers']['show_fields']).to be_an(Array)
305
+ expect(parsed['controllers']['form_fields']).to be_an(Array)
306
+
307
+ # Check form_fields structure
308
+ form_field = parsed['controllers']['form_fields'].find { |f| f['name'] == 'title' }
309
+ expect(form_field['type']).to eq('string')
310
+
311
+ uploader_field = parsed['controllers']['form_fields'].find { |f| f['name'] == 'images' }
312
+ expect(uploader_field['type']).to eq('files')
313
+ end
314
+ end
315
+ end
316
+
317
+ it 'generates controllers section with inline empty arrays when empty' do
318
+ Dir.mktmpdir do |dir|
319
+ Dir.chdir(dir) do
320
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
321
+ FileUtils.mkdir_p('app/models/models')
322
+
323
+ columns = [
324
+ TestColumn.new('id', :uuid),
325
+ TestColumn.new('created_at', :datetime),
326
+ TestColumn.new('updated_at', :datetime)
327
+ ]
328
+ create_test_model(class_name: 'Article', columns: columns)
329
+
330
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner:account', 'resource_owner_id:account_id'])
331
+ allow(instance).to receive(:options).and_return({ engine: nil })
332
+ instance.generate_use_case
333
+
334
+ yaml_content = File.read('db/structures/articles_structure.yaml')
335
+ parsed = YAML.load(yaml_content)
336
+ # Should be inline format for empty arrays
337
+ expect(yaml_content).to match(/^\s+list_fields:\s*\[\]/m)
338
+ # show_fields will always have at least id, created_at, updated_at
339
+ expect(parsed['controllers']['show_fields']).to be_an(Array)
340
+ expect(parsed['controllers']['show_fields']).to include('id', 'created_at', 'updated_at')
341
+ expect(yaml_content).to match(/^\s+form_fields:\s*\[\]/m)
342
+ end
343
+ end
344
+ end
345
+
346
+ it 'generates domains section with all action contracts' do
347
+ Dir.mktmpdir do |dir|
348
+ Dir.chdir(dir) do
349
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
350
+ FileUtils.mkdir_p('app/models/models')
351
+
352
+ create_test_model(class_name: 'Article', columns: create_test_columns)
353
+
354
+ instance = klass.new(['Models::Article', 'actor:owner', 'search_able:title', 'resource_owner:account', 'resource_owner_id:account_id'])
355
+ allow(instance).to receive(:options).and_return({ engine: nil })
356
+ instance.generate_use_case
357
+
358
+ yaml_content = File.read('db/structures/articles_structure.yaml')
359
+ parsed = YAML.load(yaml_content)
360
+
361
+ expect(parsed['domains']).to be_a(Hash)
362
+
363
+ # Check action_list
364
+ expect(parsed['domains']['action_list']['use_case']['contract']).to be_an(Array)
365
+ expect(parsed['domains']['action_list']['repository']['filters']).to be_an(Array)
366
+
367
+ # Check action_fetch_by_id
368
+ expect(parsed['domains']['action_fetch_by_id']['use_case']['contract']).to be_an(Array)
369
+ expect(parsed['domains']['action_fetch_by_id']['use_case']['contract'].first).to include('required(:id).filled(:string)')
370
+
371
+ # Check action_create
372
+ expect(parsed['domains']['action_create']['use_case']['contract']).to be_an(Array)
373
+
374
+ # Check action_update
375
+ expect(parsed['domains']['action_update']['use_case']['contract']).to be_an(Array)
376
+ expect(parsed['domains']['action_update']['use_case']['contract'].first).to include('required(:id).filled(:string)')
377
+
378
+ # Check action_destroy
379
+ expect(parsed['domains']['action_destroy']['use_case']['contract']).to be_an(Array)
380
+ expect(parsed['domains']['action_destroy']['use_case']['contract'].first).to include('required(:id).filled(:string)')
381
+ end
382
+ end
383
+ end
384
+
385
+ it 'generates action_fetch_by_id contract with required id and resource_owner_id' do
386
+ Dir.mktmpdir do |dir|
387
+ Dir.chdir(dir) do
388
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
389
+ FileUtils.mkdir_p('app/models/models')
390
+
391
+ create_test_model(class_name: 'Article', columns: create_test_columns)
392
+
393
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner_id:account_id', 'resource_owner:account'])
394
+ allow(instance).to receive(:options).and_return({ engine: nil })
395
+ instance.generate_use_case
396
+
397
+ yaml_content = File.read('db/structures/articles_structure.yaml')
398
+ parsed = YAML.load(yaml_content)
399
+
400
+ contract = parsed['domains']['action_fetch_by_id']['use_case']['contract']
401
+ expect(contract).to include(match(/required\(:id\)\.filled\(:string\)/))
402
+ expect(contract).to include(match(/required\(:account_id\)\.filled\(:string\)/))
403
+ end
404
+ end
405
+ end
406
+
407
+ it 'generates action_create contract with all fields and resource_owner_id' do
408
+ Dir.mktmpdir do |dir|
409
+ Dir.chdir(dir) do
410
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
411
+ FileUtils.mkdir_p('app/models/models')
412
+
413
+ create_test_model(class_name: 'Article', columns: create_test_columns)
414
+
415
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner_id:account_id', 'uploaders:images', 'resource_owner:account'])
416
+ allow(instance).to receive(:options).and_return({ engine: nil })
417
+ instance.generate_use_case
418
+
419
+ yaml_content = File.read('db/structures/articles_structure.yaml')
420
+ parsed = YAML.load(yaml_content)
421
+
422
+ contract = parsed['domains']['action_create']['use_case']['contract']
423
+ expect(contract).to include(match(/required\(:account_id\)\.filled\(:string\)/))
424
+ expect(contract.any? { |c| c.include?('title') }).to be true
425
+ expect(contract.any? { |c| c.include?('body') }).to be true
426
+ end
427
+ end
428
+ end
429
+
430
+ it 'generates action_update contract with required id' do
431
+ Dir.mktmpdir do |dir|
432
+ Dir.chdir(dir) do
433
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
434
+ FileUtils.mkdir_p('app/models/models')
435
+
436
+ create_test_model(class_name: 'Article', columns: create_test_columns)
437
+
438
+ instance = klass.new(['Models::Article', 'actor:owner', 'uploaders:images', 'resource_owner:account', 'resource_owner_id:account_id'])
439
+ allow(instance).to receive(:options).and_return({ engine: nil })
440
+ instance.generate_use_case
441
+
442
+ yaml_content = File.read('db/structures/articles_structure.yaml')
443
+ parsed = YAML.load(yaml_content)
444
+
445
+ contract = parsed['domains']['action_update']['use_case']['contract']
446
+ expect(contract.first).to include('required(:id).filled(:string)')
447
+ expect(contract.any? { |c| c.include?('title') }).to be true
448
+ end
449
+ end
450
+ end
451
+
452
+ it 'generates action_destroy contract with required id and resource_owner_id' do
453
+ Dir.mktmpdir do |dir|
454
+ Dir.chdir(dir) do
455
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
456
+ FileUtils.mkdir_p('app/models/models')
457
+
458
+ create_test_model(class_name: 'Article', columns: create_test_columns)
459
+
460
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner_id:account_id', 'resource_owner:account'])
461
+ allow(instance).to receive(:options).and_return({ engine: nil })
462
+ instance.generate_use_case
463
+
464
+ yaml_content = File.read('db/structures/articles_structure.yaml')
465
+ parsed = YAML.load(yaml_content)
466
+
467
+ contract = parsed['domains']['action_destroy']['use_case']['contract']
468
+ expect(contract).to include(match(/required\(:id\)\.filled\(:string\)/))
469
+ expect(contract).to include(match(/required\(:account_id\)\.filled\(:string\)/))
470
+ end
471
+ end
472
+ end
473
+
474
+ it 'generates domains contracts with inline empty arrays when empty' do
475
+ Dir.mktmpdir do |dir|
476
+ Dir.chdir(dir) do
477
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
478
+ FileUtils.mkdir_p('app/models/models')
479
+
480
+ columns = [
481
+ TestColumn.new('id', :uuid),
482
+ TestColumn.new('created_at', :datetime),
483
+ TestColumn.new('updated_at', :datetime)
484
+ ]
485
+ create_test_model(class_name: 'Article', columns: columns)
486
+
487
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner:account', 'resource_owner_id:account_id'])
488
+ allow(instance).to receive(:options).and_return({ engine: nil })
489
+ instance.generate_use_case
490
+
491
+ yaml_content = File.read('db/structures/articles_structure.yaml')
492
+
493
+ # Should be inline format for empty contract arrays
494
+ expect(yaml_content).to match(/^\s+contract:\s*\[\]/m)
495
+ expect(yaml_content).to match(/^\s+filters:\s*\[\]/m)
496
+ end
497
+ end
498
+ end
499
+
500
+ it 'generates entity section with skipped_fields and db_attributes' do
501
+ Dir.mktmpdir do |dir|
502
+ Dir.chdir(dir) do
503
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
504
+ FileUtils.mkdir_p('app/models/models')
505
+
506
+ create_test_model(class_name: 'Article', columns: create_test_columns)
507
+
508
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner:account', 'resource_owner_id:account_id'])
509
+ allow(instance).to receive(:options).and_return({ engine: nil })
510
+ instance.generate_use_case
511
+
512
+ yaml_content = File.read('db/structures/articles_structure.yaml')
513
+ parsed = YAML.load(yaml_content)
514
+
515
+ expect(parsed['entity']).to be_a(Hash)
516
+ expect(parsed['entity']['skipped_fields']).to include('id', 'created_at', 'updated_at')
517
+ expect(parsed['entity']['db_attributes']).to be_an(Array)
518
+ expect(parsed['entity']['db_attributes']).to include('account_id', 'title', 'body')
519
+ end
520
+ end
521
+ end
522
+
523
+ it 'generates entity db_attributes with inline empty array when empty' do
524
+ Dir.mktmpdir do |dir|
525
+ Dir.chdir(dir) do
526
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
527
+ FileUtils.mkdir_p('app/models/models')
528
+
529
+ columns = [
530
+ TestColumn.new('id', :uuid),
531
+ TestColumn.new('created_at', :datetime),
532
+ TestColumn.new('updated_at', :datetime)
533
+ ]
534
+ create_test_model(class_name: 'Article', columns: columns)
535
+
536
+ instance = klass.new(['Models::Article', 'actor:owner', 'resource_owner:account', 'resource_owner_id:account_id'])
537
+ allow(instance).to receive(:options).and_return({ engine: nil })
538
+ instance.generate_use_case
539
+
540
+ yaml_content = File.read('db/structures/articles_structure.yaml')
541
+
542
+ # Should be inline format: db_attributes: []
543
+ expect(yaml_content).to match(/^\s+db_attributes:\s*\[\]/m)
544
+ end
545
+ end
546
+ end
547
+
548
+ it 'generates valid YAML that can be parsed without errors' do
549
+ Dir.mktmpdir do |dir|
550
+ Dir.chdir(dir) do
551
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
552
+ FileUtils.mkdir_p('app/models/models')
553
+
554
+ create_test_model(class_name: 'Article', columns: create_test_columns)
555
+
556
+ instance = klass.new(['Models::Article', 'actor:owner', 'uploaders:images,assets', 'search_able:title,body', 'resource_owner_id:account_id', 'resource_owner:account'])
557
+ allow(instance).to receive(:options).and_return({ engine: nil })
558
+ instance.generate_use_case
559
+
560
+ yaml_content = File.read('db/structures/articles_structure.yaml')
561
+
562
+ # Should parse without errors
563
+ expect { YAML.load(yaml_content) }.not_to raise_error
564
+
565
+ parsed = YAML.load(yaml_content)
566
+ expect(parsed).to be_a(Hash)
567
+ end
568
+ end
569
+ end
570
+
571
+ it 'generates correct YAML indentation (2 spaces per level)' do
572
+ Dir.mktmpdir do |dir|
573
+ Dir.chdir(dir) do
574
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
575
+ FileUtils.mkdir_p('app/models/models')
576
+
577
+ create_test_model(class_name: 'Article', columns: create_test_columns)
578
+
579
+ instance = klass.new(['Models::Article', 'actor:owner', 'uploaders:images', 'resource_owner:account', 'resource_owner_id:account_id'])
580
+ allow(instance).to receive(:options).and_return({ engine: nil })
581
+ instance.generate_use_case
582
+
583
+ yaml_content = File.read('db/structures/articles_structure.yaml')
584
+
585
+ # Check indentation for nested sections
586
+ lines = yaml_content.split("\n")
587
+ schema_line = lines.find { |l| l.include?('schema:') }
588
+ lines.index(schema_line)
589
+ columns_line = lines.find { |l| l.include?('columns:') }
590
+ columns_index = lines.index(columns_line)
591
+
592
+ # columns should be indented 2 spaces from schema
593
+ expect(lines[columns_index]).to start_with(' columns:')
594
+ end
595
+ end
596
+ end
597
+ end
598
+ end