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,820 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+ require 'generators/rider_kick/scaffold_generator'
7
+
8
+ RSpec.describe 'rider_kick:scaffold conditional filtering' do
9
+ let(:klass) { RiderKick::ScaffoldGenerator }
10
+
11
+ # Helper untuk setup test model (mirip dengan scaffold_generator_success_spec.rb)
12
+ def setup_test_model(class_name: 'Article', columns: [])
13
+ Object.send(:remove_const, :Models) if Object.const_defined?(:Models)
14
+ Object.send(:remove_const, :TestColumn) if Object.const_defined?(:TestColumn)
15
+
16
+ # Create Models module using Module.new to avoid syntax error
17
+ models_module = Module.new
18
+ Object.const_set(:Models, models_module)
19
+
20
+ # Create TestColumn struct
21
+ test_column_struct = Struct.new(:name, :type, :sql_type, :null, :default, :precision, :scale, :limit)
22
+ Object.const_set(:TestColumn, test_column_struct)
23
+
24
+ # Remove existing class if it exists
25
+ model_class_name = "Models::#{class_name}"
26
+ if Object.const_defined?(model_class_name)
27
+ parts = model_class_name.split('::')
28
+ if parts.length > 1
29
+ parent = Object.const_get(parts[0..-2].join('::'))
30
+ parent.send(:remove_const, parts.last.to_sym) if parent.const_defined?(parts.last.to_sym)
31
+ end
32
+ end
33
+
34
+ # Create model class directly (like in scaffold_generator_success_spec.rb)
35
+ model_class = Class.new do
36
+ define_singleton_method(:columns) { columns }
37
+ define_singleton_method(:columns_hash) do
38
+ columns.to_h { |c| [c.name.to_s, Struct.new(:type).new(c.type)] }
39
+ end
40
+ define_singleton_method(:column_names) { columns.map { |c| c.name.to_s } }
41
+ end
42
+
43
+ Models.const_set(class_name, model_class)
44
+ created_class = Models.const_get(class_name)
45
+
46
+ # Verify that constantize works (this is what validate_model_exists! will do)
47
+ model_class_name.constantize
48
+
49
+ created_class
50
+ end
51
+
52
+ describe 'field_exists_in_contract? method' do
53
+ it 'returns true when field exists in contract as required' do
54
+ Dir.mktmpdir do |dir|
55
+ Dir.chdir(dir) do
56
+ FileUtils.mkdir_p([
57
+ RiderKick.configuration.domains_path + '/core/use_cases',
58
+ RiderKick.configuration.domains_path + '/core/repositories',
59
+ RiderKick.configuration.domains_path + '/core/builders',
60
+ RiderKick.configuration.domains_path + '/core/entities',
61
+ 'app/models/models',
62
+ 'db/structures'
63
+ ])
64
+
65
+ setup_test_model(class_name: 'Article', columns: [
66
+ TestColumn.new('id', :uuid),
67
+ TestColumn.new('account_id', :uuid),
68
+ TestColumn.new('title', :string),
69
+ TestColumn.new('created_at', :datetime),
70
+ TestColumn.new('updated_at', :datetime)
71
+ ])
72
+
73
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
74
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
75
+ model: Models::Article
76
+ resource_name: articles
77
+ actor: user
78
+ resource_owner_id: account_id
79
+ resource_owner: account
80
+ uploaders: []
81
+ search_able: []
82
+ domains:
83
+ action_list:
84
+ use_case:
85
+ contract:
86
+ - "required(:account_id).filled(:string)"
87
+ - "optional(:title).maybe(:string)"
88
+ action_fetch_by_id:
89
+ use_case:
90
+ contract: []
91
+ action_create:
92
+ use_case:
93
+ contract: []
94
+ action_update:
95
+ use_case:
96
+ contract: []
97
+ action_destroy:
98
+ use_case:
99
+ contract: []
100
+ entity:
101
+ db_attributes: [id, created_at, updated_at]
102
+ YAML
103
+
104
+ instance = klass.new(['articles'])
105
+ instance.send(:setup_variables)
106
+
107
+ # Test field_exists_in_contract? directly
108
+ expect(instance.send(:field_exists_in_contract?, 'account_id', 'list')).to be true
109
+ expect(instance.send(:field_exists_in_contract?, 'account_id', 'fetch_by_id')).to be false
110
+ expect(instance.send(:field_exists_in_contract?, 'title', 'list')).to be true
111
+ expect(instance.send(:field_exists_in_contract?, 'nonexistent', 'list')).to be false
112
+ end
113
+ end
114
+ end
115
+
116
+ it 'returns true when field exists in contract as optional' do
117
+ Dir.mktmpdir do |dir|
118
+ Dir.chdir(dir) do
119
+ FileUtils.mkdir_p([
120
+ RiderKick.configuration.domains_path + '/core/use_cases',
121
+ RiderKick.configuration.domains_path + '/core/repositories',
122
+ RiderKick.configuration.domains_path + '/core/builders',
123
+ RiderKick.configuration.domains_path + '/core/entities',
124
+ 'app/models/models',
125
+ 'db/structures'
126
+ ])
127
+
128
+ setup_test_model(class_name: 'Article', columns: [
129
+ TestColumn.new('id', :uuid),
130
+ TestColumn.new('account_id', :uuid),
131
+ TestColumn.new('created_at', :datetime),
132
+ TestColumn.new('updated_at', :datetime)
133
+ ])
134
+
135
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
136
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
137
+ model: Models::Article
138
+ resource_name: articles
139
+ actor: user
140
+ resource_owner_id: account_id
141
+ resource_owner: account
142
+ uploaders: []
143
+ search_able: []
144
+ domains:
145
+ action_list:
146
+ use_case:
147
+ contract:
148
+ - "optional(:account_id).maybe(:string)"
149
+ action_fetch_by_id:
150
+ use_case:
151
+ contract: []
152
+ action_create:
153
+ use_case:
154
+ contract: []
155
+ action_update:
156
+ use_case:
157
+ contract: []
158
+ action_destroy:
159
+ use_case:
160
+ contract: []
161
+ entity:
162
+ db_attributes: [id, created_at, updated_at]
163
+ YAML
164
+
165
+ instance = klass.new(['articles'])
166
+ instance.send(:setup_variables)
167
+
168
+ expect(instance.send(:field_exists_in_contract?, 'account_id', 'list')).to be true
169
+ end
170
+ end
171
+ end
172
+
173
+ it 'returns false when field_name is blank' do
174
+ Dir.mktmpdir do |dir|
175
+ Dir.chdir(dir) do
176
+ FileUtils.mkdir_p([
177
+ RiderKick.configuration.domains_path + '/core/use_cases',
178
+ RiderKick.configuration.domains_path + '/core/repositories',
179
+ RiderKick.configuration.domains_path + '/core/builders',
180
+ RiderKick.configuration.domains_path + '/core/entities',
181
+ 'app/models/models',
182
+ 'db/structures'
183
+ ])
184
+
185
+ setup_test_model(class_name: 'Article', columns: [
186
+ TestColumn.new('id', :uuid),
187
+ TestColumn.new('created_at', :datetime),
188
+ TestColumn.new('updated_at', :datetime)
189
+ ])
190
+
191
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
192
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
193
+ model: Models::Article
194
+ resource_name: articles
195
+ actor: user
196
+ resource_owner_id:
197
+ resource_owner: account
198
+ uploaders: []
199
+ search_able: []
200
+ domains:
201
+ action_list:
202
+ use_case:
203
+ contract: []
204
+ action_fetch_by_id:
205
+ use_case:
206
+ contract: []
207
+ action_create:
208
+ use_case:
209
+ contract: []
210
+ action_update:
211
+ use_case:
212
+ contract: []
213
+ action_destroy:
214
+ use_case:
215
+ contract: []
216
+ entity:
217
+ db_attributes: [id, created_at, updated_at]
218
+ YAML
219
+
220
+ instance = klass.new(['articles'])
221
+ instance.send(:setup_variables)
222
+
223
+ expect(instance.send(:field_exists_in_contract?, nil, 'list')).to be false
224
+ expect(instance.send(:field_exists_in_contract?, '', 'list')).to be false
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ describe 'conditional resource_owner_id filtering' do
231
+ it 'does NOT use resource_owner_id filter when field is NOT in contract' do
232
+ Dir.mktmpdir do |dir|
233
+ Dir.chdir(dir) do
234
+ FileUtils.mkdir_p([
235
+ RiderKick.configuration.domains_path + '/core/use_cases',
236
+ RiderKick.configuration.domains_path + '/core/repositories',
237
+ RiderKick.configuration.domains_path + '/core/builders',
238
+ RiderKick.configuration.domains_path + '/core/entities',
239
+ 'app/models/models',
240
+ 'db/structures'
241
+ ])
242
+
243
+ setup_test_model(class_name: 'Article', columns: [
244
+ TestColumn.new('id', :uuid),
245
+ TestColumn.new('account_id', :uuid),
246
+ TestColumn.new('title', :string),
247
+ TestColumn.new('created_at', :datetime),
248
+ TestColumn.new('updated_at', :datetime)
249
+ ])
250
+
251
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
252
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
253
+ model: Models::Article
254
+ resource_name: articles
255
+ actor: user
256
+ resource_owner_id: account_id
257
+ resource_owner: account
258
+ uploaders: []
259
+ search_able: []
260
+ domains:
261
+ action_list:
262
+ use_case:
263
+ contract:
264
+ - "optional(:title).maybe(:string)"
265
+ # account_id TIDAK ADA di contract
266
+ action_fetch_by_id:
267
+ use_case:
268
+ contract: []
269
+ action_create:
270
+ use_case:
271
+ contract: []
272
+ action_update:
273
+ use_case:
274
+ contract: []
275
+ action_destroy:
276
+ use_case:
277
+ contract: []
278
+ entity:
279
+ db_attributes: [id, created_at, updated_at]
280
+ YAML
281
+
282
+ klass.new(['articles']).generate_use_case
283
+
284
+ # Check list repository - should NOT have resource_owner_id filter
285
+ list_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/list_article.rb')
286
+ expect(list_repo).not_to match(/\.where\(account_id:/)
287
+ expect(list_repo).to match(/resources = Models::Article\s*$/)
288
+
289
+ # Check fetch_by_id repository - should NOT have resource_owner_id filter
290
+ fetch_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/fetch_article_by_id.rb')
291
+ expect(fetch_repo).not_to match(/find_by\(id: @id, account_id:/)
292
+ expect(fetch_repo).to match(/find_by\(id: @id\)/)
293
+
294
+ # Check update repository - should NOT have resource_owner_id filter
295
+ update_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/update_article.rb')
296
+ expect(update_repo).not_to match(/find_by\(id: @id, account_id:/)
297
+ expect(update_repo).to match(/find_by\(id: @id\)/)
298
+
299
+ # Check destroy repository - should NOT have resource_owner_id filter
300
+ destroy_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/destroy_article.rb')
301
+ expect(destroy_repo).not_to match(/find_by\(id: @id, account_id:/)
302
+ expect(destroy_repo).to match(/find_by\(id: @id\)/)
303
+ end
304
+ end
305
+ end
306
+
307
+ it 'DOES use resource_owner_id filter when field IS in contract' do
308
+ Dir.mktmpdir do |dir|
309
+ Dir.chdir(dir) do
310
+ FileUtils.mkdir_p([
311
+ RiderKick.configuration.domains_path + '/core/use_cases',
312
+ RiderKick.configuration.domains_path + '/core/repositories',
313
+ RiderKick.configuration.domains_path + '/core/builders',
314
+ RiderKick.configuration.domains_path + '/core/entities',
315
+ 'app/models/models',
316
+ 'db/structures'
317
+ ])
318
+
319
+ setup_test_model(class_name: 'Article', columns: [
320
+ TestColumn.new('id', :uuid),
321
+ TestColumn.new('account_id', :uuid),
322
+ TestColumn.new('title', :string),
323
+ TestColumn.new('created_at', :datetime),
324
+ TestColumn.new('updated_at', :datetime)
325
+ ])
326
+
327
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
328
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
329
+ model: Models::Article
330
+ resource_name: articles
331
+ actor: user
332
+ resource_owner_id: account_id
333
+ resource_owner: account
334
+ uploaders: []
335
+ search_able: []
336
+ domains:
337
+ action_list:
338
+ use_case:
339
+ contract:
340
+ - "required(:account_id).filled(:string)"
341
+ - "optional(:title).maybe(:string)"
342
+ action_fetch_by_id:
343
+ use_case:
344
+ contract:
345
+ - "required(:account_id).filled(:string)"
346
+ action_update:
347
+ use_case:
348
+ contract:
349
+ - "required(:account_id).filled(:string)"
350
+ action_destroy:
351
+ use_case:
352
+ contract:
353
+ - "required(:account_id).filled(:string)"
354
+ action_create:
355
+ use_case:
356
+ contract: []
357
+ entity:
358
+ db_attributes: [id, created_at, updated_at]
359
+ YAML
360
+
361
+ klass.new(['articles']).generate_use_case
362
+
363
+ # Check list repository - SHOULD have resource_owner_id filter
364
+ list_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/list_article.rb')
365
+ expect(list_repo).to match(/\.where\(account_id: @params\.account_id\)/)
366
+
367
+ # Check fetch_by_id repository - SHOULD have resource_owner_id filter
368
+ fetch_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/fetch_article_by_id.rb')
369
+ expect(fetch_repo).to match(/find_by\(id: @id, account_id: @params\.account_id\)/)
370
+
371
+ # Check update repository - SHOULD have resource_owner_id filter
372
+ update_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/update_article.rb')
373
+ expect(update_repo).to match(/find_by\(id: @id, account_id: @params\.account_id\)/)
374
+
375
+ # Check destroy repository - SHOULD have resource_owner_id filter
376
+ destroy_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/destroy_article.rb')
377
+ expect(destroy_repo).to match(/find_by\(id: @id, account_id: @params\.account_id\)/)
378
+ end
379
+ end
380
+ end
381
+
382
+ it 'uses resource_owner_id filter only for actions where it exists in contract' do
383
+ Dir.mktmpdir do |dir|
384
+ Dir.chdir(dir) do
385
+ FileUtils.mkdir_p([
386
+ RiderKick.configuration.domains_path + '/core/use_cases',
387
+ RiderKick.configuration.domains_path + '/core/repositories',
388
+ RiderKick.configuration.domains_path + '/core/builders',
389
+ RiderKick.configuration.domains_path + '/core/entities',
390
+ 'app/models/models',
391
+ 'db/structures'
392
+ ])
393
+
394
+ setup_test_model(class_name: 'Article', columns: [
395
+ TestColumn.new('id', :uuid),
396
+ TestColumn.new('account_id', :uuid),
397
+ TestColumn.new('title', :string),
398
+ TestColumn.new('created_at', :datetime),
399
+ TestColumn.new('updated_at', :datetime)
400
+ ])
401
+
402
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
403
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
404
+ model: Models::Article
405
+ resource_name: articles
406
+ actor: user
407
+ resource_owner_id: account_id
408
+ resource_owner: account
409
+ uploaders: []
410
+ search_able: []
411
+ domains:
412
+ action_list:
413
+ use_case:
414
+ contract:
415
+ - "required(:account_id).filled(:string)"
416
+ action_fetch_by_id:
417
+ use_case:
418
+ contract: []
419
+ # account_id TIDAK ADA di contract untuk fetch_by_id
420
+ action_create:
421
+ use_case:
422
+ contract: []
423
+ action_update:
424
+ use_case:
425
+ contract:
426
+ - "required(:account_id).filled(:string)"
427
+ action_destroy:
428
+ use_case:
429
+ contract: []
430
+ entity:
431
+ db_attributes: [id, created_at, updated_at]
432
+ YAML
433
+
434
+ klass.new(['articles']).generate_use_case
435
+
436
+ # List should have filter
437
+ list_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/list_article.rb')
438
+ expect(list_repo).to match(/\.where\(account_id: @params\.account_id\)/)
439
+
440
+ # Fetch_by_id should NOT have filter
441
+ fetch_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/fetch_article_by_id.rb')
442
+ expect(fetch_repo).not_to match(/find_by\(id: @id, account_id:/)
443
+ expect(fetch_repo).to match(/find_by\(id: @id\)/)
444
+
445
+ # Update should have filter
446
+ update_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/update_article.rb')
447
+ expect(update_repo).to match(/find_by\(id: @id, account_id: @params\.account_id\)/)
448
+
449
+ # Destroy should NOT have filter
450
+ destroy_repo = File.read(RiderKick.configuration.domains_path + '/repositories/articles/destroy_article.rb')
451
+ expect(destroy_repo).not_to match(/find_by\(id: @id, account_id:/)
452
+ expect(destroy_repo).to match(/find_by\(id: @id\)/)
453
+ end
454
+ end
455
+ end
456
+ end
457
+
458
+ describe 'instance variables setup' do
459
+ it 'sets @actor_id correctly from structure.actor_id' do
460
+ Dir.mktmpdir do |dir|
461
+ Dir.chdir(dir) do
462
+ FileUtils.mkdir_p([
463
+ RiderKick.configuration.domains_path + '/core/use_cases',
464
+ RiderKick.configuration.domains_path + '/core/repositories',
465
+ RiderKick.configuration.domains_path + '/core/builders',
466
+ RiderKick.configuration.domains_path + '/core/entities',
467
+ 'app/models/models',
468
+ 'db/structures'
469
+ ])
470
+
471
+ setup_test_model(class_name: 'Article', columns: [
472
+ TestColumn.new('id', :uuid),
473
+ TestColumn.new('created_at', :datetime),
474
+ TestColumn.new('updated_at', :datetime)
475
+ ])
476
+
477
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
478
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
479
+ model: Models::Article
480
+ resource_name: articles
481
+ actor: user
482
+ actor_id: custom_user_id
483
+ resource_owner_id: account_id
484
+ resource_owner: account
485
+ uploaders: []
486
+ search_able: []
487
+ domains:
488
+ action_list:
489
+ use_case:
490
+ contract: []
491
+ action_fetch_by_id:
492
+ use_case:
493
+ contract: []
494
+ action_create:
495
+ use_case:
496
+ contract: []
497
+ action_update:
498
+ use_case:
499
+ contract: []
500
+ action_destroy:
501
+ use_case:
502
+ contract: []
503
+ entity:
504
+ db_attributes: [id, created_at, updated_at]
505
+ YAML
506
+
507
+ instance = klass.new(['articles'])
508
+ instance.send(:setup_variables)
509
+
510
+ expect(instance.instance_variable_get(:@actor_id)).to eq('custom_user_id')
511
+ end
512
+ end
513
+ end
514
+
515
+ it 'generates @actor_id from @actor when actor_id not present in structure' do
516
+ Dir.mktmpdir do |dir|
517
+ Dir.chdir(dir) do
518
+ FileUtils.mkdir_p([
519
+ RiderKick.configuration.domains_path + '/core/use_cases',
520
+ RiderKick.configuration.domains_path + '/core/repositories',
521
+ RiderKick.configuration.domains_path + '/core/builders',
522
+ RiderKick.configuration.domains_path + '/core/entities',
523
+ 'app/models/models',
524
+ 'db/structures'
525
+ ])
526
+
527
+ setup_test_model(class_name: 'Article', columns: [
528
+ TestColumn.new('id', :uuid),
529
+ TestColumn.new('created_at', :datetime),
530
+ TestColumn.new('updated_at', :datetime)
531
+ ])
532
+
533
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
534
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
535
+ model: Models::Article
536
+ resource_name: articles
537
+ actor: admin
538
+ resource_owner_id: account_id
539
+ resource_owner: account
540
+ uploaders: []
541
+ search_able: []
542
+ domains:
543
+ action_list:
544
+ use_case:
545
+ contract: []
546
+ action_fetch_by_id:
547
+ use_case:
548
+ contract: []
549
+ action_create:
550
+ use_case:
551
+ contract: []
552
+ action_update:
553
+ use_case:
554
+ contract: []
555
+ action_destroy:
556
+ use_case:
557
+ contract: []
558
+ entity:
559
+ db_attributes: [id, created_at, updated_at]
560
+ YAML
561
+
562
+ instance = klass.new(['articles'])
563
+ instance.send(:setup_variables)
564
+
565
+ expect(instance.instance_variable_get(:@actor_id)).to eq('admin_id')
566
+ end
567
+ end
568
+ end
569
+
570
+ it 'sets @has_resource_owner_id_in_*_contract flags correctly' do
571
+ Dir.mktmpdir do |dir|
572
+ Dir.chdir(dir) do
573
+ FileUtils.mkdir_p([
574
+ RiderKick.configuration.domains_path + '/core/use_cases',
575
+ RiderKick.configuration.domains_path + '/core/repositories',
576
+ RiderKick.configuration.domains_path + '/core/builders',
577
+ RiderKick.configuration.domains_path + '/core/entities',
578
+ 'app/models/models',
579
+ 'db/structures'
580
+ ])
581
+
582
+ setup_test_model(class_name: 'Article', columns: [
583
+ TestColumn.new('id', :uuid),
584
+ TestColumn.new('account_id', :uuid),
585
+ TestColumn.new('created_at', :datetime),
586
+ TestColumn.new('updated_at', :datetime)
587
+ ])
588
+
589
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
590
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
591
+ model: Models::Article
592
+ resource_name: articles
593
+ actor: user
594
+ resource_owner_id: account_id
595
+ resource_owner: account
596
+ uploaders: []
597
+ search_able: []
598
+ domains:
599
+ action_list:
600
+ use_case:
601
+ contract:
602
+ - "required(:account_id).filled(:string)"
603
+ action_fetch_by_id:
604
+ use_case:
605
+ contract: []
606
+ action_create:
607
+ use_case:
608
+ contract:
609
+ - "required(:account_id).filled(:string)"
610
+ action_update:
611
+ use_case:
612
+ contract: []
613
+ action_destroy:
614
+ use_case:
615
+ contract:
616
+ - "required(:account_id).filled(:string)"
617
+ entity:
618
+ db_attributes: [id, created_at, updated_at]
619
+ YAML
620
+
621
+ instance = klass.new(['articles'])
622
+ instance.send(:setup_variables)
623
+
624
+ expect(instance.instance_variable_get(:@has_resource_owner_id_in_list_contract)).to be true
625
+ expect(instance.instance_variable_get(:@has_resource_owner_id_in_fetch_by_id_contract)).to be false
626
+ expect(instance.instance_variable_get(:@has_resource_owner_id_in_create_contract)).to be true
627
+ expect(instance.instance_variable_get(:@has_resource_owner_id_in_update_contract)).to be false
628
+ expect(instance.instance_variable_get(:@has_resource_owner_id_in_destroy_contract)).to be true
629
+ end
630
+ end
631
+ end
632
+ end
633
+
634
+ describe 'helper methods' do
635
+ it 'has_resource_owner_id_in_contract? returns correct values' do
636
+ Dir.mktmpdir do |dir|
637
+ Dir.chdir(dir) do
638
+ FileUtils.mkdir_p([
639
+ RiderKick.configuration.domains_path + '/core/use_cases',
640
+ RiderKick.configuration.domains_path + '/core/repositories',
641
+ RiderKick.configuration.domains_path + '/core/builders',
642
+ RiderKick.configuration.domains_path + '/core/entities',
643
+ 'app/models/models',
644
+ 'db/structures'
645
+ ])
646
+
647
+ setup_test_model(class_name: 'Article', columns: [
648
+ TestColumn.new('id', :uuid),
649
+ TestColumn.new('account_id', :uuid),
650
+ TestColumn.new('created_at', :datetime),
651
+ TestColumn.new('updated_at', :datetime)
652
+ ])
653
+
654
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
655
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
656
+ model: Models::Article
657
+ resource_name: articles
658
+ actor: user
659
+ resource_owner_id: account_id
660
+ resource_owner: account
661
+ uploaders: []
662
+ search_able: []
663
+ domains:
664
+ action_list:
665
+ use_case:
666
+ contract:
667
+ - "required(:account_id).filled(:string)"
668
+ action_fetch_by_id:
669
+ use_case:
670
+ contract: []
671
+ action_create:
672
+ use_case:
673
+ contract: []
674
+ action_update:
675
+ use_case:
676
+ contract: []
677
+ action_destroy:
678
+ use_case:
679
+ contract: []
680
+ entity:
681
+ db_attributes: [id, created_at, updated_at]
682
+ YAML
683
+
684
+ instance = klass.new(['articles'])
685
+ instance.send(:setup_variables)
686
+
687
+ expect(instance.send(:has_resource_owner_id_in_contract?, 'list')).to be true
688
+ expect(instance.send(:has_resource_owner_id_in_contract?, 'fetch_by_id')).to be false
689
+ expect(instance.send(:has_resource_owner_id_in_contract?, 'fetch')).to be false
690
+ expect(instance.send(:has_resource_owner_id_in_contract?, 'create')).to be false
691
+ expect(instance.send(:has_resource_owner_id_in_contract?, 'update')).to be false
692
+ expect(instance.send(:has_resource_owner_id_in_contract?, 'destroy')).to be false
693
+ expect(instance.send(:has_resource_owner_id_in_contract?, 'unknown')).to be false
694
+ end
695
+ end
696
+ end
697
+ end
698
+
699
+ describe 'use case list contract generation' do
700
+ it 'includes resource_owner_id in contract when it exists in contract list' do
701
+ Dir.mktmpdir do |dir|
702
+ Dir.chdir(dir) do
703
+ FileUtils.mkdir_p([
704
+ RiderKick.configuration.domains_path + '/core/use_cases',
705
+ RiderKick.configuration.domains_path + '/core/repositories',
706
+ RiderKick.configuration.domains_path + '/core/builders',
707
+ RiderKick.configuration.domains_path + '/core/entities',
708
+ 'app/models/models',
709
+ 'db/structures'
710
+ ])
711
+
712
+ setup_test_model(class_name: 'Article', columns: [
713
+ TestColumn.new('id', :uuid),
714
+ TestColumn.new('account_id', :uuid),
715
+ TestColumn.new('created_at', :datetime),
716
+ TestColumn.new('updated_at', :datetime)
717
+ ])
718
+
719
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
720
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
721
+ model: Models::Article
722
+ resource_name: articles
723
+ actor: user
724
+ resource_owner_id: account_id
725
+ resource_owner: account
726
+ uploaders: []
727
+ search_able: []
728
+ domains:
729
+ action_list:
730
+ use_case:
731
+ contract:
732
+ - "required(:account_id).filled(:string)"
733
+ action_fetch_by_id:
734
+ use_case:
735
+ contract: []
736
+ action_create:
737
+ use_case:
738
+ contract: []
739
+ action_update:
740
+ use_case:
741
+ contract: []
742
+ action_destroy:
743
+ use_case:
744
+ contract: []
745
+ entity:
746
+ db_attributes: [id, created_at, updated_at]
747
+ YAML
748
+
749
+ klass.new(['articles']).generate_use_case
750
+
751
+ list_usecase = File.read(RiderKick.configuration.domains_path + '/use_cases/articles/user_list_article.rb')
752
+ # Should include the contract line from YAML
753
+ expect(list_usecase).to include('required(:account_id).filled(:string)')
754
+ # Should NOT add duplicate resource_owner_id since it's already in contract
755
+ # (The template should check @has_resource_owner_id_in_list_contract)
756
+ end
757
+ end
758
+ end
759
+
760
+ it 'does NOT include resource_owner_id in contract when it does NOT exist in contract list' do
761
+ Dir.mktmpdir do |dir|
762
+ Dir.chdir(dir) do
763
+ FileUtils.mkdir_p([
764
+ RiderKick.configuration.domains_path + '/core/use_cases',
765
+ RiderKick.configuration.domains_path + '/core/repositories',
766
+ RiderKick.configuration.domains_path + '/core/builders',
767
+ RiderKick.configuration.domains_path + '/core/entities',
768
+ 'app/models/models',
769
+ 'db/structures'
770
+ ])
771
+
772
+ setup_test_model(class_name: 'Article', columns: [
773
+ TestColumn.new('id', :uuid),
774
+ TestColumn.new('account_id', :uuid),
775
+ TestColumn.new('title', :string),
776
+ TestColumn.new('created_at', :datetime),
777
+ TestColumn.new('updated_at', :datetime)
778
+ ])
779
+
780
+ File.write('app/models/models/article.rb', "class Models::Article < ApplicationRecord; end\n")
781
+ File.write('db/structures/articles_structure.yaml', <<~YAML)
782
+ model: Models::Article
783
+ resource_name: articles
784
+ actor: user
785
+ resource_owner_id: account_id
786
+ resource_owner: account
787
+ uploaders: []
788
+ search_able: []
789
+ domains:
790
+ action_list:
791
+ use_case:
792
+ contract:
793
+ - "optional(:title).maybe(:string)"
794
+ # account_id TIDAK ADA di contract
795
+ action_fetch_by_id:
796
+ use_case:
797
+ contract: []
798
+ action_create:
799
+ use_case:
800
+ contract: []
801
+ action_update:
802
+ use_case:
803
+ contract: []
804
+ action_destroy:
805
+ use_case:
806
+ contract: []
807
+ entity:
808
+ db_attributes: [id, created_at, updated_at]
809
+ YAML
810
+
811
+ klass.new(['articles']).generate_use_case
812
+
813
+ list_usecase = File.read(RiderKick.configuration.domains_path + '/use_cases/articles/user_list_article.rb')
814
+ # Should NOT include resource_owner_id since it's not in contract
815
+ expect(list_usecase).not_to match(/required\(:account_id\)\.filled\(:string\)/)
816
+ end
817
+ end
818
+ end
819
+ end
820
+ end