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,2202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+ require 'generators/rider_kick/structure_generator'
7
+
8
+ # Define Column struct at top level to avoid dynamic constant assignment warning
9
+ TestColumn = Struct.new(:name, :type, :sql_type, :null, :default, :precision, :scale, :limit) unless defined?(TestColumn)
10
+
11
+ RSpec.describe 'rider_kick:structure generator (unit tests)' do
12
+ let(:klass) { RiderKick::Structure }
13
+
14
+ def create_test_model(module_name: 'Models', class_name: 'Article', columns: [])
15
+ # Remove nested modules properly
16
+ parts = module_name.split('::')
17
+ if parts.length > 1
18
+ # For nested modules, remove from innermost to outermost
19
+ begin
20
+ Object.const_get(module_name)
21
+ # If exists, remove the class first
22
+ full_name = "#{module_name}::#{class_name}"
23
+ if Object.const_defined?(full_name)
24
+ Object.const_get(module_name).send(:remove_const, class_name.to_sym)
25
+ end
26
+ # Then remove modules from innermost
27
+ (parts.length - 1).downto(0) do |i|
28
+ module_path = parts[0..i].join('::')
29
+ if Object.const_defined?(module_path)
30
+ parent = i > 0 ? Object.const_get(parts[0..i - 1].join('::')) : Object
31
+ parent.send(:remove_const, parts[i].to_sym)
32
+ end
33
+ end
34
+ rescue NameError
35
+ # Module doesn't exist, continue
36
+ end
37
+ elsif Object.const_defined?(module_name.to_sym)
38
+ Object.send(:remove_const, module_name.to_sym)
39
+ end
40
+
41
+ # Create nested modules if needed
42
+ parts = module_name.split('::')
43
+ current_module = Object
44
+ parts.each do |part|
45
+ unless current_module.const_defined?(part.to_sym)
46
+ current_module.const_set(part.to_sym, Module.new)
47
+ end
48
+ current_module = current_module.const_get(part.to_sym)
49
+ end
50
+
51
+ model_class = Class.new do
52
+ define_singleton_method(:columns) { columns }
53
+
54
+ define_singleton_method(:columns_hash) do
55
+ columns.to_h { |c| [c.name.to_s, Struct.new(:type).new(c.type)] }
56
+ end
57
+
58
+ define_singleton_method(:column_names) do
59
+ columns.map { |c| c.name.to_s }
60
+ end
61
+ end
62
+
63
+ current_module.const_set(class_name.to_sym, model_class)
64
+ "#{module_name}::#{class_name}".constantize
65
+ end
66
+
67
+ def create_test_columns
68
+ [
69
+ TestColumn.new('id', :uuid, 'uuid', false),
70
+ TestColumn.new('account_id', :uuid, 'uuid', true),
71
+ TestColumn.new('title', :string, 'character varying', true),
72
+ TestColumn.new('body', :text, 'text', true),
73
+ TestColumn.new('published_at', :datetime, 'timestamp without time zone', true),
74
+ TestColumn.new('user_id', :uuid, 'uuid', true),
75
+ TestColumn.new('images', :text, 'text', true),
76
+ TestColumn.new('created_at', :datetime, 'timestamp without time zone', false),
77
+ TestColumn.new('updated_at', :datetime, 'timestamp without time zone', false)
78
+ ]
79
+ end
80
+
81
+ before do
82
+ RiderKick.configuration.engine_name = nil
83
+ RiderKick.configuration.domain_scope = ''
84
+ end
85
+
86
+ describe '#setup_variables' do
87
+ context 'with simple model name' do
88
+ it 'sets @variable_subject correctly for simple model name' do
89
+ Dir.mktmpdir do |dir|
90
+ Dir.chdir(dir) do
91
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
92
+ FileUtils.mkdir_p('app/models/models')
93
+
94
+ create_test_model(class_name: 'User', columns: create_test_columns)
95
+
96
+ instance = klass.new(['Models::User', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
97
+ allow(instance).to receive(:options).and_return({ engine: nil })
98
+ instance.send(:setup_variables)
99
+
100
+ expect(instance.instance_variable_get(:@variable_subject)).to eq('user')
101
+ end
102
+ end
103
+ end
104
+
105
+ it 'sets @model_class correctly' do
106
+ Dir.mktmpdir do |dir|
107
+ Dir.chdir(dir) do
108
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
109
+ FileUtils.mkdir_p('app/models/models')
110
+
111
+ model_class = create_test_model(class_name: 'User', columns: create_test_columns)
112
+
113
+ instance = klass.new(['Models::User', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
114
+ allow(instance).to receive(:options).and_return({ engine: nil })
115
+ instance.send(:setup_variables)
116
+
117
+ expect(instance.instance_variable_get(:@model_class)).to eq(model_class)
118
+ end
119
+ end
120
+ end
121
+
122
+ it 'sets @subject_class correctly' do
123
+ Dir.mktmpdir do |dir|
124
+ Dir.chdir(dir) do
125
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
126
+ FileUtils.mkdir_p('app/models/models')
127
+
128
+ create_test_model(class_name: 'User', columns: create_test_columns)
129
+
130
+ instance = klass.new(['Models::User', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
131
+ allow(instance).to receive(:options).and_return({ engine: nil })
132
+ instance.send(:setup_variables)
133
+
134
+ expect(instance.instance_variable_get(:@subject_class)).to eq('User')
135
+ end
136
+ end
137
+ end
138
+
139
+ it 'sets @scope_path correctly with pluralization' do
140
+ Dir.mktmpdir do |dir|
141
+ Dir.chdir(dir) do
142
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
143
+ FileUtils.mkdir_p('app/models/models')
144
+
145
+ create_test_model(class_name: 'User', columns: create_test_columns)
146
+
147
+ instance = klass.new(['Models::User', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
148
+ allow(instance).to receive(:options).and_return({ engine: nil })
149
+ instance.send(:setup_variables)
150
+
151
+ expect(instance.instance_variable_get(:@scope_path)).to eq('users')
152
+ end
153
+ end
154
+ end
155
+
156
+ it 'sets @scope_class correctly with camelization' 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: 'User', columns: create_test_columns)
163
+
164
+ instance = klass.new(['Models::User', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
165
+ allow(instance).to receive(:options).and_return({ engine: nil })
166
+ instance.send(:setup_variables)
167
+
168
+ expect(instance.instance_variable_get(:@scope_class)).to eq('Users')
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ context 'with complex namespace' do
175
+ it 'sets @variable_subject correctly for complex namespace' do
176
+ Dir.mktmpdir do |dir|
177
+ Dir.chdir(dir) do
178
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
179
+ FileUtils.mkdir_p('app/models/models')
180
+
181
+ create_test_model(module_name: 'Models::Core', class_name: 'User', columns: create_test_columns)
182
+
183
+ instance = klass.new(['Models::Core::User', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
184
+ allow(instance).to receive(:options).and_return({ engine: nil })
185
+ instance.send(:setup_variables)
186
+
187
+ expect(instance.instance_variable_get(:@variable_subject)).to eq('user')
188
+ end
189
+ end
190
+ end
191
+
192
+ it 'sets @subject_class correctly for complex namespace' do
193
+ Dir.mktmpdir do |dir|
194
+ Dir.chdir(dir) do
195
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
196
+ FileUtils.mkdir_p('app/models/models')
197
+
198
+ create_test_model(module_name: 'Models::Core', class_name: 'User', columns: create_test_columns)
199
+
200
+ instance = klass.new(['Models::Core::User', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
201
+ allow(instance).to receive(:options).and_return({ engine: nil })
202
+ instance.send(:setup_variables)
203
+
204
+ expect(instance.instance_variable_get(:@subject_class)).to eq('User')
205
+ end
206
+ end
207
+ end
208
+ end
209
+
210
+ it 'sets @fields from contract_fields' do
211
+ Dir.mktmpdir do |dir|
212
+ Dir.chdir(dir) do
213
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
214
+ FileUtils.mkdir_p('app/models/models')
215
+
216
+ create_test_model(class_name: 'Article', columns: create_test_columns)
217
+
218
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
219
+ allow(instance).to receive(:options).and_return({ engine: nil })
220
+ instance.send(:setup_variables)
221
+
222
+ fields = instance.instance_variable_get(:@fields)
223
+ expect(fields).to be_an(Array)
224
+ expect(fields).not_to include('id', 'created_at', 'updated_at')
225
+ expect(fields).to include('account_id', 'title', 'body')
226
+ end
227
+ end
228
+ end
229
+
230
+ it 'sets @uploaders as array string' do
231
+ Dir.mktmpdir do |dir|
232
+ Dir.chdir(dir) do
233
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
234
+ FileUtils.mkdir_p('app/models/models')
235
+
236
+ create_test_model(class_name: 'Article', columns: create_test_columns)
237
+
238
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:images,assets', 'resource_owner:account', 'resource_owner_id:account_id'])
239
+ allow(instance).to receive(:options).and_return({ engine: nil })
240
+ instance.send(:setup_variables)
241
+
242
+ uploaders = instance.instance_variable_get(:@uploaders)
243
+ expect(uploaders).to eq(['images', 'assets'])
244
+ end
245
+ end
246
+ end
247
+
248
+ it 'sets @actor correctly with lowercase conversion' do
249
+ Dir.mktmpdir do |dir|
250
+ Dir.chdir(dir) do
251
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
252
+ FileUtils.mkdir_p('app/models/models')
253
+
254
+ create_test_model(class_name: 'Article', columns: create_test_columns)
255
+
256
+ instance = klass.new(['Models::Article', 'actor:User', 'resource_owner:account', 'resource_owner_id:account_id'])
257
+ allow(instance).to receive(:options).and_return({ engine: nil })
258
+ instance.send(:setup_variables)
259
+
260
+ expect(instance.instance_variable_get(:@actor)).to eq('user')
261
+ end
262
+ end
263
+ end
264
+
265
+ it 'sets @actor_id correctly when actor is present' do
266
+ Dir.mktmpdir do |dir|
267
+ Dir.chdir(dir) do
268
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
269
+ FileUtils.mkdir_p('app/models/models')
270
+
271
+ create_test_model(class_name: 'Article', columns: create_test_columns)
272
+
273
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
274
+ allow(instance).to receive(:options).and_return({ engine: nil })
275
+ instance.send(:setup_variables)
276
+
277
+ expect(instance.instance_variable_get(:@actor_id)).to eq('user_id')
278
+ end
279
+ end
280
+ end
281
+
282
+ it 'sets @actor_id to empty string when actor is blank' do
283
+ Dir.mktmpdir do |dir|
284
+ Dir.chdir(dir) do
285
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
286
+ FileUtils.mkdir_p('app/models/models')
287
+
288
+ create_test_model(class_name: 'Article', columns: create_test_columns)
289
+
290
+ instance = klass.new(['Models::Article', 'actor:', 'resource_owner:account', 'resource_owner_id:account_id'])
291
+ allow(instance).to receive(:options).and_return({ engine: nil })
292
+ instance.send(:setup_variables)
293
+
294
+ expect(instance.instance_variable_get(:@actor_id)).to eq('')
295
+ end
296
+ end
297
+ end
298
+
299
+ it 'sets @resource_owner and @resource_owner_id from settings' do
300
+ Dir.mktmpdir do |dir|
301
+ Dir.chdir(dir) do
302
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
303
+ FileUtils.mkdir_p('app/models/models')
304
+
305
+ create_test_model(class_name: 'Article', columns: create_test_columns)
306
+
307
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
308
+ allow(instance).to receive(:options).and_return({ engine: nil })
309
+ instance.send(:setup_variables)
310
+
311
+ expect(instance.instance_variable_get(:@resource_owner)).to eq('account')
312
+ expect(instance.instance_variable_get(:@resource_owner_id)).to eq('account_id')
313
+ end
314
+ end
315
+ end
316
+
317
+ it 'sets @columns from columns_meta' 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 = create_test_columns
324
+ create_test_model(class_name: 'Article', columns: columns)
325
+
326
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
327
+ allow(instance).to receive(:options).and_return({ engine: nil })
328
+ instance.send(:setup_variables)
329
+
330
+ columns_meta = instance.instance_variable_get(:@columns)
331
+ expect(columns_meta).to be_an(Array)
332
+ expect(columns_meta.length).to eq(columns.length)
333
+ expect(columns_meta.first[:name]).to eq('id')
334
+ end
335
+ end
336
+ end
337
+
338
+ it 'sets @type_mapping and @entity_type_mapping' do
339
+ Dir.mktmpdir do |dir|
340
+ Dir.chdir(dir) do
341
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
342
+ FileUtils.mkdir_p('app/models/models')
343
+
344
+ create_test_model(class_name: 'Article', columns: create_test_columns)
345
+
346
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
347
+ allow(instance).to receive(:options).and_return({ engine: nil })
348
+ instance.send(:setup_variables)
349
+
350
+ expect(instance.instance_variable_get(:@type_mapping)).to eq(RiderKick::TYPE_MAPPING)
351
+ expect(instance.instance_variable_get(:@entity_type_mapping)).to eq(RiderKick::ENTITY_TYPE_MAPPING)
352
+ end
353
+ end
354
+ end
355
+
356
+ it 'sets @columns_meta_hash with correct indexing structure' do
357
+ Dir.mktmpdir do |dir|
358
+ Dir.chdir(dir) do
359
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
360
+ FileUtils.mkdir_p('app/models/models')
361
+
362
+ create_test_model(class_name: 'Article', columns: create_test_columns)
363
+
364
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
365
+ allow(instance).to receive(:options).and_return({ engine: nil })
366
+ instance.send(:setup_variables)
367
+
368
+ columns_meta_hash = instance.instance_variable_get(:@columns_meta_hash)
369
+ expect(columns_meta_hash).to be_a(Hash)
370
+ expect(columns_meta_hash['id']).to be_a(Hash)
371
+ expect(columns_meta_hash['id'][:name]).to eq('id')
372
+ end
373
+ end
374
+ end
375
+ end
376
+
377
+ describe '@contract_lines_for_create generation' do
378
+ it 'generates required field for null: false' do
379
+ Dir.mktmpdir do |dir|
380
+ Dir.chdir(dir) do
381
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
382
+ FileUtils.mkdir_p('app/models/models')
383
+
384
+ columns = [
385
+ TestColumn.new('id', :uuid, 'uuid', false),
386
+ TestColumn.new('title', :string, 'character varying', false),
387
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
388
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
389
+ ]
390
+ create_test_model(class_name: 'Article', columns: columns)
391
+
392
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
393
+ allow(instance).to receive(:options).and_return({ engine: nil })
394
+ instance.send(:setup_variables)
395
+
396
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_create)
397
+ title_line = contract_lines.find { |l| l.include?('title') }
398
+ expect(title_line).to include('required(:title)')
399
+ expect(title_line).to include('.filled')
400
+ end
401
+ end
402
+ end
403
+
404
+ it 'generates optional field for null: true' do
405
+ Dir.mktmpdir do |dir|
406
+ Dir.chdir(dir) do
407
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
408
+ FileUtils.mkdir_p('app/models/models')
409
+
410
+ columns = [
411
+ TestColumn.new('id', :uuid, 'uuid', false),
412
+ TestColumn.new('title', :string, 'character varying', true),
413
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
414
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
415
+ ]
416
+ create_test_model(class_name: 'Article', columns: columns)
417
+
418
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
419
+ allow(instance).to receive(:options).and_return({ engine: nil })
420
+ instance.send(:setup_variables)
421
+
422
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_create)
423
+ title_line = contract_lines.find { |l| l.include?('title') }
424
+ expect(title_line).to include('optional(:title)')
425
+ expect(title_line).to include('.maybe')
426
+ end
427
+ end
428
+ end
429
+
430
+ it 'generates Types::File for upload field' 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
+ columns = [
437
+ TestColumn.new('id', :uuid, 'uuid', false),
438
+ TestColumn.new('title', :string, 'character varying', true),
439
+ TestColumn.new('image', :string, 'character varying', false),
440
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
441
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
442
+ ]
443
+ create_test_model(class_name: 'Article', columns: columns)
444
+
445
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:image', 'resource_owner:account', 'resource_owner_id:account_id'])
446
+ allow(instance).to receive(:options).and_return({ engine: nil })
447
+ instance.send(:setup_variables)
448
+
449
+ # Test get_column_type directly - should return 'upload' for uploader field
450
+ column_type = instance.send(:get_column_type, 'image')
451
+ expect(column_type).to eq('upload')
452
+
453
+ # Test that uploaders are excluded from contract_fields (by design)
454
+ fields = instance.instance_variable_get(:@fields)
455
+ expect(fields).not_to include('image')
456
+ expect(fields).to include('title')
457
+ end
458
+ end
459
+ end
460
+
461
+ it 'generates optional Types::File for upload field with null: true' do
462
+ Dir.mktmpdir do |dir|
463
+ Dir.chdir(dir) do
464
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
465
+ FileUtils.mkdir_p('app/models/models')
466
+
467
+ columns = [
468
+ TestColumn.new('id', :uuid, 'uuid', false),
469
+ TestColumn.new('title', :string, 'character varying', true),
470
+ TestColumn.new('image', :string, 'character varying', true),
471
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
472
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
473
+ ]
474
+ create_test_model(class_name: 'Article', columns: columns)
475
+
476
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:image', 'resource_owner:account', 'resource_owner_id:account_id'])
477
+ allow(instance).to receive(:options).and_return({ engine: nil })
478
+ instance.send(:setup_variables)
479
+
480
+ # Test get_column_type directly - should return 'upload' for uploader field
481
+ column_type = instance.send(:get_column_type, 'image')
482
+ expect(column_type).to eq('upload')
483
+
484
+ # Uploaders are excluded from contract_fields by design
485
+ fields = instance.instance_variable_get(:@fields)
486
+ expect(fields).not_to include('image')
487
+ end
488
+ end
489
+ end
490
+
491
+ it 'falls back to :string for unknown db_type' do
492
+ Dir.mktmpdir do |dir|
493
+ Dir.chdir(dir) do
494
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
495
+ FileUtils.mkdir_p('app/models/models')
496
+
497
+ columns = [
498
+ TestColumn.new('id', :uuid, 'uuid', false),
499
+ TestColumn.new('custom_field', :unknown_type, 'unknown', true),
500
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
501
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
502
+ ]
503
+ create_test_model(class_name: 'Article', columns: columns)
504
+
505
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
506
+ allow(instance).to receive(:options).and_return({ engine: nil })
507
+ instance.send(:setup_variables)
508
+
509
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_create)
510
+ custom_line = contract_lines.find { |l| l.include?('custom_field') }
511
+ expect(custom_line).to include(':string')
512
+ end
513
+ end
514
+ end
515
+
516
+ it 'handles various db_types correctly' do
517
+ Dir.mktmpdir do |dir|
518
+ Dir.chdir(dir) do
519
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
520
+ FileUtils.mkdir_p('app/models/models')
521
+
522
+ columns = [
523
+ TestColumn.new('id', :uuid, 'uuid', false),
524
+ TestColumn.new('uuid_field', :uuid, 'uuid', true),
525
+ TestColumn.new('int_field', :integer, 'integer', true),
526
+ TestColumn.new('bool_field', :boolean, 'boolean', true),
527
+ TestColumn.new('decimal_field', :decimal, 'decimal', true),
528
+ TestColumn.new('date_field', :date, 'date', true),
529
+ TestColumn.new('datetime_field', :datetime, 'timestamp', true),
530
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
531
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
532
+ ]
533
+ create_test_model(class_name: 'Article', columns: columns)
534
+
535
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
536
+ allow(instance).to receive(:options).and_return({ engine: nil })
537
+ instance.send(:setup_variables)
538
+
539
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_create)
540
+
541
+ uuid_line = contract_lines.find { |l| l.include?('uuid_field') }
542
+ expect(uuid_line).to include(':string')
543
+
544
+ int_line = contract_lines.find { |l| l.include?('int_field') }
545
+ expect(int_line).to include(':integer')
546
+
547
+ bool_line = contract_lines.find { |l| l.include?('bool_field') }
548
+ expect(bool_line).to include(':bool')
549
+
550
+ decimal_line = contract_lines.find { |l| l.include?('decimal_field') }
551
+ expect(decimal_line).to include(':decimal')
552
+
553
+ date_line = contract_lines.find { |l| l.include?('date_field') }
554
+ expect(date_line).to include(':date')
555
+
556
+ datetime_line = contract_lines.find { |l| l.include?('datetime_field') }
557
+ expect(datetime_line).to include(':time')
558
+ end
559
+ end
560
+ end
561
+
562
+ it 'skips field that does not exist in @columns_meta_hash' do
563
+ Dir.mktmpdir do |dir|
564
+ Dir.chdir(dir) do
565
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
566
+ FileUtils.mkdir_p('app/models/models')
567
+
568
+ columns = [
569
+ TestColumn.new('id', :uuid, 'uuid', false),
570
+ TestColumn.new('title', :string, 'character varying', true),
571
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
572
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
573
+ ]
574
+ create_test_model(class_name: 'Article', columns: columns)
575
+
576
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
577
+ allow(instance).to receive(:options).and_return({ engine: nil })
578
+ instance.send(:setup_variables)
579
+
580
+ # Manually add a field that doesn't exist in columns
581
+ fields = instance.instance_variable_get(:@fields)
582
+ fields << 'nonexistent_field'
583
+ instance.instance_variable_set(:@fields, fields)
584
+
585
+ # Re-run contract lines generation logic
586
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_create)
587
+ nonexistent_line = contract_lines.find { |l| l.include?('nonexistent_field') }
588
+ expect(nonexistent_line).to be_nil
589
+ end
590
+ end
591
+ end
592
+
593
+ it 'removes nil values with compact' do
594
+ Dir.mktmpdir do |dir|
595
+ Dir.chdir(dir) do
596
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
597
+ FileUtils.mkdir_p('app/models/models')
598
+
599
+ columns = [
600
+ TestColumn.new('id', :uuid, 'uuid', false),
601
+ TestColumn.new('title', :string, 'character varying', true),
602
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
603
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
604
+ ]
605
+ create_test_model(class_name: 'Article', columns: columns)
606
+
607
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
608
+ allow(instance).to receive(:options).and_return({ engine: nil })
609
+ instance.send(:setup_variables)
610
+
611
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_create)
612
+ expect(contract_lines).not_to include(nil)
613
+ expect(contract_lines.all? { |l| l.is_a?(String) }).to be true
614
+ end
615
+ end
616
+ end
617
+ end
618
+
619
+ describe '@contract_lines_for_update generation' do
620
+ it 'makes all fields optional' do
621
+ Dir.mktmpdir do |dir|
622
+ Dir.chdir(dir) do
623
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
624
+ FileUtils.mkdir_p('app/models/models')
625
+
626
+ columns = [
627
+ TestColumn.new('id', :uuid, 'uuid', false),
628
+ TestColumn.new('title', :string, 'character varying', false),
629
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
630
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
631
+ ]
632
+ create_test_model(class_name: 'Article', columns: columns)
633
+
634
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
635
+ allow(instance).to receive(:options).and_return({ engine: nil })
636
+ instance.send(:setup_variables)
637
+
638
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_update)
639
+ title_line = contract_lines.find { |l| l.include?('title') }
640
+ expect(title_line).to include('optional(:title)')
641
+ expect(title_line).to include('.maybe')
642
+ end
643
+ end
644
+ end
645
+
646
+ it 'handles upload field in update' do
647
+ Dir.mktmpdir do |dir|
648
+ Dir.chdir(dir) do
649
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
650
+ FileUtils.mkdir_p('app/models/models')
651
+
652
+ columns = [
653
+ TestColumn.new('id', :uuid, 'uuid', false),
654
+ TestColumn.new('title', :string, 'character varying', true),
655
+ TestColumn.new('image', :string, 'character varying', false),
656
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
657
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
658
+ ]
659
+ create_test_model(class_name: 'Article', columns: columns)
660
+
661
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:image', 'resource_owner:account', 'resource_owner_id:account_id'])
662
+ allow(instance).to receive(:options).and_return({ engine: nil })
663
+ instance.send(:setup_variables)
664
+
665
+ # Test get_column_type directly - should return 'upload' for uploader field
666
+ column_type = instance.send(:get_column_type, 'image')
667
+ expect(column_type).to eq('upload')
668
+
669
+ # Uploaders are excluded from contract_fields by design, so they won't appear in update contract lines
670
+ # But the logic for handling upload types is tested via get_column_type
671
+ end
672
+ end
673
+ end
674
+
675
+ it 'falls back to :string for unknown db_type in update' do
676
+ Dir.mktmpdir do |dir|
677
+ Dir.chdir(dir) do
678
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
679
+ FileUtils.mkdir_p('app/models/models')
680
+
681
+ columns = [
682
+ TestColumn.new('id', :uuid, 'uuid', false),
683
+ TestColumn.new('custom_field', :unknown_type, 'unknown', true),
684
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
685
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
686
+ ]
687
+ create_test_model(class_name: 'Article', columns: columns)
688
+
689
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
690
+ allow(instance).to receive(:options).and_return({ engine: nil })
691
+ instance.send(:setup_variables)
692
+
693
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_update)
694
+ custom_line = contract_lines.find { |l| l.include?('custom_field') }
695
+ expect(custom_line).to include(':string')
696
+ end
697
+ end
698
+ end
699
+ end
700
+
701
+ describe '@contract_lines_for_list generation' do
702
+ it 'generates from search_able settings' do
703
+ Dir.mktmpdir do |dir|
704
+ Dir.chdir(dir) do
705
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
706
+ FileUtils.mkdir_p('app/models/models')
707
+
708
+ create_test_model(class_name: 'Article', columns: create_test_columns)
709
+
710
+ instance = klass.new(['Models::Article', 'actor:user', 'search_able:title,body', 'resource_owner:account', 'resource_owner_id:account_id'])
711
+ allow(instance).to receive(:options).and_return({ engine: nil })
712
+ instance.send(:setup_variables)
713
+
714
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_list)
715
+ expect(contract_lines.length).to eq(2)
716
+ expect(contract_lines.any? { |l| l.include?('title') }).to be true
717
+ expect(contract_lines.any? { |l| l.include?('body') }).to be true
718
+ end
719
+ end
720
+ end
721
+
722
+ it 'parses from @repository_list_filters string format' do
723
+ Dir.mktmpdir do |dir|
724
+ Dir.chdir(dir) do
725
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
726
+ FileUtils.mkdir_p('app/models/models')
727
+
728
+ create_test_model(class_name: 'Article', columns: create_test_columns)
729
+
730
+ instance = klass.new(['Models::Article', 'actor:user', 'search_able:title', 'resource_owner:account', 'resource_owner_id:account_id'])
731
+ allow(instance).to receive(:options).and_return({ engine: nil })
732
+ instance.send(:setup_variables)
733
+
734
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_list)
735
+ title_line = contract_lines.find { |l| l.include?('title') }
736
+ expect(title_line).to include('optional(:title)')
737
+ expect(title_line).to include('.maybe(:string)')
738
+ end
739
+ end
740
+ end
741
+
742
+ it 'returns empty array when search_able is empty' do
743
+ Dir.mktmpdir do |dir|
744
+ Dir.chdir(dir) do
745
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
746
+ FileUtils.mkdir_p('app/models/models')
747
+
748
+ create_test_model(class_name: 'Article', columns: create_test_columns)
749
+
750
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
751
+ allow(instance).to receive(:options).and_return({ engine: nil })
752
+ instance.send(:setup_variables)
753
+
754
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_list)
755
+ expect(contract_lines).to eq([])
756
+ end
757
+ end
758
+ end
759
+
760
+ it 'handles multiple search fields' do
761
+ Dir.mktmpdir do |dir|
762
+ Dir.chdir(dir) do
763
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
764
+ FileUtils.mkdir_p('app/models/models')
765
+
766
+ create_test_model(class_name: 'Article', columns: create_test_columns)
767
+
768
+ instance = klass.new(['Models::Article', 'actor:user', 'search_able:title,body,published_at', 'resource_owner:account', 'resource_owner_id:account_id'])
769
+ allow(instance).to receive(:options).and_return({ engine: nil })
770
+ instance.send(:setup_variables)
771
+
772
+ contract_lines = instance.instance_variable_get(:@contract_lines_for_list)
773
+ expect(contract_lines.length).to eq(3)
774
+ end
775
+ end
776
+ end
777
+ end
778
+
779
+ describe '@repository_list_filters generation' do
780
+ it 'generates from search_able settings' do
781
+ Dir.mktmpdir do |dir|
782
+ Dir.chdir(dir) do
783
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
784
+ FileUtils.mkdir_p('app/models/models')
785
+
786
+ create_test_model(class_name: 'Article', columns: create_test_columns)
787
+
788
+ instance = klass.new(['Models::Article', 'actor:user', 'search_able:title', 'resource_owner:account', 'resource_owner_id:account_id'])
789
+ allow(instance).to receive(:options).and_return({ engine: nil })
790
+ instance.send(:setup_variables)
791
+
792
+ filters = instance.instance_variable_get(:@repository_list_filters)
793
+ expect(filters).to include("{ field: 'title', type: 'search' }")
794
+ end
795
+ end
796
+ end
797
+
798
+ it 'handles multiple fields with comma-separated' do
799
+ Dir.mktmpdir do |dir|
800
+ Dir.chdir(dir) do
801
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
802
+ FileUtils.mkdir_p('app/models/models')
803
+
804
+ create_test_model(class_name: 'Article', columns: create_test_columns)
805
+
806
+ instance = klass.new(['Models::Article', 'actor:user', 'search_able:title,body', 'resource_owner:account', 'resource_owner_id:account_id'])
807
+ allow(instance).to receive(:options).and_return({ engine: nil })
808
+ instance.send(:setup_variables)
809
+
810
+ filters = instance.instance_variable_get(:@repository_list_filters)
811
+ expect(filters.length).to eq(2)
812
+ expect(filters).to include("{ field: 'title', type: 'search' }")
813
+ expect(filters).to include("{ field: 'body', type: 'search' }")
814
+ end
815
+ end
816
+ end
817
+
818
+ it 'trims whitespace' do
819
+ Dir.mktmpdir do |dir|
820
+ Dir.chdir(dir) do
821
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
822
+ FileUtils.mkdir_p('app/models/models')
823
+
824
+ create_test_model(class_name: 'Article', columns: create_test_columns)
825
+
826
+ instance = klass.new(['Models::Article', 'actor:user', 'search_able: title , body ', 'resource_owner:account', 'resource_owner_id:account_id'])
827
+ allow(instance).to receive(:options).and_return({ engine: nil })
828
+ instance.send(:setup_variables)
829
+
830
+ filters = instance.instance_variable_get(:@repository_list_filters)
831
+ expect(filters).to include("{ field: 'title', type: 'search' }")
832
+ expect(filters).to include("{ field: 'body', type: 'search' }")
833
+ end
834
+ end
835
+ end
836
+
837
+ it 'returns empty array when search_able is empty' do
838
+ Dir.mktmpdir do |dir|
839
+ Dir.chdir(dir) do
840
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
841
+ FileUtils.mkdir_p('app/models/models')
842
+
843
+ create_test_model(class_name: 'Article', columns: create_test_columns)
844
+
845
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
846
+ allow(instance).to receive(:options).and_return({ engine: nil })
847
+ instance.send(:setup_variables)
848
+
849
+ filters = instance.instance_variable_get(:@repository_list_filters)
850
+ expect(filters).to eq([])
851
+ end
852
+ end
853
+ end
854
+ end
855
+
856
+ describe '@entity_uploader_definitions generation' do
857
+ it 'generates single type for singular uploader' do
858
+ Dir.mktmpdir do |dir|
859
+ Dir.chdir(dir) do
860
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
861
+ FileUtils.mkdir_p('app/models/models')
862
+
863
+ create_test_model(class_name: 'Article', columns: create_test_columns)
864
+
865
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:image', 'resource_owner:account', 'resource_owner_id:account_id'])
866
+ allow(instance).to receive(:options).and_return({ engine: nil })
867
+ instance.send(:setup_variables)
868
+
869
+ uploader_defs = instance.instance_variable_get(:@entity_uploader_definitions)
870
+ image_def = uploader_defs.find { |u| u[:name] == 'image' }
871
+ expect(image_def).to eq({ name: 'image', type: 'single' })
872
+ end
873
+ end
874
+ end
875
+
876
+ it 'generates multiple type for plural uploader' do
877
+ Dir.mktmpdir do |dir|
878
+ Dir.chdir(dir) do
879
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
880
+ FileUtils.mkdir_p('app/models/models')
881
+
882
+ create_test_model(class_name: 'Article', columns: create_test_columns)
883
+
884
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:images', 'resource_owner:account', 'resource_owner_id:account_id'])
885
+ allow(instance).to receive(:options).and_return({ engine: nil })
886
+ instance.send(:setup_variables)
887
+
888
+ uploader_defs = instance.instance_variable_get(:@entity_uploader_definitions)
889
+ images_def = uploader_defs.find { |u| u[:name] == 'images' }
890
+ expect(images_def).to eq({ name: 'images', type: 'multiple' })
891
+ end
892
+ end
893
+ end
894
+
895
+ it 'handles multiple uploaders with mix singular/plural' do
896
+ Dir.mktmpdir do |dir|
897
+ Dir.chdir(dir) do
898
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
899
+ FileUtils.mkdir_p('app/models/models')
900
+
901
+ create_test_model(class_name: 'Article', columns: create_test_columns)
902
+
903
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:image,images,asset,assets', 'resource_owner:account', 'resource_owner_id:account_id'])
904
+ allow(instance).to receive(:options).and_return({ engine: nil })
905
+ instance.send(:setup_variables)
906
+
907
+ uploader_defs = instance.instance_variable_get(:@entity_uploader_definitions)
908
+ expect(uploader_defs.length).to eq(4)
909
+
910
+ image_def = uploader_defs.find { |u| u[:name] == 'image' }
911
+ expect(image_def[:type]).to eq('single')
912
+
913
+ images_def = uploader_defs.find { |u| u[:name] == 'images' }
914
+ expect(images_def[:type]).to eq('multiple')
915
+
916
+ asset_def = uploader_defs.find { |u| u[:name] == 'asset' }
917
+ expect(asset_def[:type]).to eq('single')
918
+
919
+ assets_def = uploader_defs.find { |u| u[:name] == 'assets' }
920
+ expect(assets_def[:type]).to eq('multiple')
921
+ end
922
+ end
923
+ end
924
+
925
+ it 'returns empty array when uploaders is empty' do
926
+ Dir.mktmpdir do |dir|
927
+ Dir.chdir(dir) do
928
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
929
+ FileUtils.mkdir_p('app/models/models')
930
+
931
+ create_test_model(class_name: 'Article', columns: create_test_columns)
932
+
933
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
934
+ allow(instance).to receive(:options).and_return({ engine: nil })
935
+ instance.send(:setup_variables)
936
+
937
+ uploader_defs = instance.instance_variable_get(:@entity_uploader_definitions)
938
+ expect(uploader_defs).to eq([])
939
+ end
940
+ end
941
+ end
942
+ end
943
+
944
+ describe '#contract_fields' do
945
+ it 'excludes id, created_at, updated_at' do
946
+ Dir.mktmpdir do |dir|
947
+ Dir.chdir(dir) do
948
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
949
+ FileUtils.mkdir_p('app/models/models')
950
+
951
+ columns = [
952
+ TestColumn.new('id', :uuid, 'uuid', false),
953
+ TestColumn.new('title', :string, 'character varying', true),
954
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
955
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
956
+ ]
957
+ create_test_model(class_name: 'Article', columns: columns)
958
+
959
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
960
+ allow(instance).to receive(:options).and_return({ engine: nil })
961
+ instance.send(:setup_variables) # Need to setup variables first to initialize @model_class
962
+
963
+ fields = instance.send(:contract_fields)
964
+ expect(fields).not_to include('id', 'created_at', 'updated_at')
965
+ expect(fields).to include('title')
966
+ end
967
+ end
968
+ end
969
+
970
+ it 'excludes type column (STI)' do
971
+ Dir.mktmpdir do |dir|
972
+ Dir.chdir(dir) do
973
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
974
+ FileUtils.mkdir_p('app/models/models')
975
+
976
+ columns = [
977
+ TestColumn.new('id', :uuid, 'uuid', false),
978
+ TestColumn.new('type', :string, 'character varying', true),
979
+ TestColumn.new('title', :string, 'character varying', true),
980
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
981
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
982
+ ]
983
+ create_test_model(class_name: 'Article', columns: columns)
984
+
985
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
986
+ allow(instance).to receive(:options).and_return({ engine: nil })
987
+ instance.send(:setup_variables)
988
+
989
+ fields = instance.send(:contract_fields)
990
+ expect(fields).not_to include('type')
991
+ expect(fields).to include('title')
992
+ end
993
+ end
994
+ end
995
+
996
+ it 'excludes uploaders from contract fields' do
997
+ Dir.mktmpdir do |dir|
998
+ Dir.chdir(dir) do
999
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1000
+ FileUtils.mkdir_p('app/models/models')
1001
+
1002
+ columns = [
1003
+ TestColumn.new('id', :uuid, 'uuid', false),
1004
+ TestColumn.new('title', :string, 'character varying', true),
1005
+ TestColumn.new('image', :string, 'character varying', true),
1006
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1007
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1008
+ ]
1009
+ create_test_model(class_name: 'Article', columns: columns)
1010
+
1011
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:image', 'resource_owner:account', 'resource_owner_id:account_id'])
1012
+ allow(instance).to receive(:options).and_return({ engine: nil })
1013
+ instance.send(:setup_variables)
1014
+
1015
+ fields = instance.send(:contract_fields)
1016
+ expect(fields).not_to include('image')
1017
+ expect(fields).to include('title')
1018
+ end
1019
+ end
1020
+ end
1021
+
1022
+ it 'returns only column names as strings' do
1023
+ Dir.mktmpdir do |dir|
1024
+ Dir.chdir(dir) do
1025
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1026
+ FileUtils.mkdir_p('app/models/models')
1027
+
1028
+ columns = [
1029
+ TestColumn.new('id', :uuid, 'uuid', false),
1030
+ TestColumn.new('title', :string, 'character varying', true),
1031
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1032
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1033
+ ]
1034
+ create_test_model(class_name: 'Article', columns: columns)
1035
+
1036
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1037
+ allow(instance).to receive(:options).and_return({ engine: nil })
1038
+ instance.send(:setup_variables)
1039
+
1040
+ fields = instance.send(:contract_fields)
1041
+ expect(fields).to all(be_a(String))
1042
+ expect(fields).to include('title')
1043
+ end
1044
+ end
1045
+ end
1046
+
1047
+ it 'returns empty result when only id/timestamps exist' do
1048
+ Dir.mktmpdir do |dir|
1049
+ Dir.chdir(dir) do
1050
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1051
+ FileUtils.mkdir_p('app/models/models')
1052
+
1053
+ columns = [
1054
+ TestColumn.new('id', :uuid, 'uuid', false),
1055
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1056
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1057
+ ]
1058
+ create_test_model(class_name: 'Article', columns: columns)
1059
+
1060
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1061
+ allow(instance).to receive(:options).and_return({ engine: nil })
1062
+ instance.send(:setup_variables)
1063
+
1064
+ fields = instance.send(:contract_fields)
1065
+ expect(fields).to eq([])
1066
+ end
1067
+ end
1068
+ end
1069
+
1070
+ it 'handles model with many columns' do
1071
+ Dir.mktmpdir do |dir|
1072
+ Dir.chdir(dir) do
1073
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1074
+ FileUtils.mkdir_p('app/models/models')
1075
+
1076
+ columns = [
1077
+ TestColumn.new('id', :uuid, 'uuid', false),
1078
+ TestColumn.new('field1', :string, 'character varying', true),
1079
+ TestColumn.new('field2', :integer, 'integer', true),
1080
+ TestColumn.new('field3', :boolean, 'boolean', true),
1081
+ TestColumn.new('field4', :decimal, 'decimal', true),
1082
+ TestColumn.new('field5', :date, 'date', true),
1083
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1084
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1085
+ ]
1086
+ create_test_model(class_name: 'Article', columns: columns)
1087
+
1088
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1089
+ allow(instance).to receive(:options).and_return({ engine: nil })
1090
+ instance.send(:setup_variables)
1091
+
1092
+ fields = instance.send(:contract_fields)
1093
+ expect(fields.length).to eq(5)
1094
+ expect(fields).to include('field1', 'field2', 'field3', 'field4', 'field5')
1095
+ end
1096
+ end
1097
+ end
1098
+ end
1099
+
1100
+ describe '#get_column_type' do
1101
+ it 'returns upload for uploader field' do
1102
+ Dir.mktmpdir do |dir|
1103
+ Dir.chdir(dir) do
1104
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1105
+ FileUtils.mkdir_p('app/models/models')
1106
+
1107
+ columns = [
1108
+ TestColumn.new('id', :uuid, 'uuid', false),
1109
+ TestColumn.new('image', :string, 'character varying', true),
1110
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1111
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1112
+ ]
1113
+ create_test_model(class_name: 'Article', columns: columns)
1114
+
1115
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:image', 'resource_owner:account', 'resource_owner_id:account_id'])
1116
+ allow(instance).to receive(:options).and_return({ engine: nil })
1117
+ instance.send(:setup_variables)
1118
+
1119
+ column_type = instance.send(:get_column_type, 'image')
1120
+ expect(column_type).to eq('upload')
1121
+ end
1122
+ end
1123
+ end
1124
+
1125
+ it 'returns actual type for non-uploader field' do
1126
+ Dir.mktmpdir do |dir|
1127
+ Dir.chdir(dir) do
1128
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1129
+ FileUtils.mkdir_p('app/models/models')
1130
+
1131
+ columns = [
1132
+ TestColumn.new('id', :uuid, 'uuid', false),
1133
+ TestColumn.new('title', :string, 'character varying', true),
1134
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1135
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1136
+ ]
1137
+ create_test_model(class_name: 'Article', columns: columns)
1138
+
1139
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1140
+ allow(instance).to receive(:options).and_return({ engine: nil })
1141
+ instance.send(:setup_variables)
1142
+
1143
+ column_type = instance.send(:get_column_type, 'title')
1144
+ expect(column_type).to eq(:string)
1145
+ end
1146
+ end
1147
+ end
1148
+
1149
+ it 'handles various types correctly' do
1150
+ Dir.mktmpdir do |dir|
1151
+ Dir.chdir(dir) do
1152
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1153
+ FileUtils.mkdir_p('app/models/models')
1154
+
1155
+ columns = [
1156
+ TestColumn.new('id', :uuid, 'uuid', false),
1157
+ TestColumn.new('uuid_field', :uuid, 'uuid', true),
1158
+ TestColumn.new('int_field', :integer, 'integer', true),
1159
+ TestColumn.new('bool_field', :boolean, 'boolean', true),
1160
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1161
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1162
+ ]
1163
+ create_test_model(class_name: 'Article', columns: columns)
1164
+
1165
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1166
+ allow(instance).to receive(:options).and_return({ engine: nil })
1167
+ instance.send(:setup_variables)
1168
+
1169
+ expect(instance.send(:get_column_type, 'uuid_field')).to eq(:uuid)
1170
+ expect(instance.send(:get_column_type, 'int_field')).to eq(:integer)
1171
+ expect(instance.send(:get_column_type, 'bool_field')).to eq(:boolean)
1172
+ end
1173
+ end
1174
+ end
1175
+ end
1176
+
1177
+ describe '#uploaders' do
1178
+ it 'parses comma-separated uploaders' do
1179
+ Dir.mktmpdir do |dir|
1180
+ Dir.chdir(dir) do
1181
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1182
+ FileUtils.mkdir_p('app/models/models')
1183
+
1184
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1185
+
1186
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:images,assets,picture', 'resource_owner:account', 'resource_owner_id:account_id'])
1187
+ allow(instance).to receive(:options).and_return({ engine: nil })
1188
+
1189
+ uploaders = instance.send(:uploaders)
1190
+ expect(uploaders).to eq(['images', 'assets', 'picture'])
1191
+ end
1192
+ end
1193
+ end
1194
+
1195
+ it 'trims whitespace' do
1196
+ Dir.mktmpdir do |dir|
1197
+ Dir.chdir(dir) do
1198
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1199
+ FileUtils.mkdir_p('app/models/models')
1200
+
1201
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1202
+
1203
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders: images , assets , picture ', 'resource_owner:account', 'resource_owner_id:account_id'])
1204
+ allow(instance).to receive(:options).and_return({ engine: nil })
1205
+
1206
+ uploaders = instance.send(:uploaders)
1207
+ expect(uploaders).to eq(['images', 'assets', 'picture'])
1208
+ end
1209
+ end
1210
+ end
1211
+
1212
+ it 'returns empty array for empty string' do
1213
+ Dir.mktmpdir do |dir|
1214
+ Dir.chdir(dir) do
1215
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1216
+ FileUtils.mkdir_p('app/models/models')
1217
+
1218
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1219
+
1220
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:', 'resource_owner:account', 'resource_owner_id:account_id'])
1221
+ allow(instance).to receive(:options).and_return({ engine: nil })
1222
+
1223
+ uploaders = instance.send(:uploaders)
1224
+ expect(uploaders).to eq([])
1225
+ end
1226
+ end
1227
+ end
1228
+
1229
+ it 'returns empty array for nil' do
1230
+ Dir.mktmpdir do |dir|
1231
+ Dir.chdir(dir) do
1232
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1233
+ FileUtils.mkdir_p('app/models/models')
1234
+
1235
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1236
+
1237
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1238
+ allow(instance).to receive(:options).and_return({ engine: nil })
1239
+
1240
+ uploaders = instance.send(:uploaders)
1241
+ expect(uploaders).to eq([])
1242
+ end
1243
+ end
1244
+ end
1245
+
1246
+ it 'returns array with single element for single uploader' do
1247
+ Dir.mktmpdir do |dir|
1248
+ Dir.chdir(dir) do
1249
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1250
+ FileUtils.mkdir_p('app/models/models')
1251
+
1252
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1253
+
1254
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:image', 'resource_owner:account', 'resource_owner_id:account_id'])
1255
+ allow(instance).to receive(:options).and_return({ engine: nil })
1256
+
1257
+ uploaders = instance.send(:uploaders)
1258
+ expect(uploaders).to eq(['image'])
1259
+ end
1260
+ end
1261
+ end
1262
+
1263
+ it 'returns array of strings' do
1264
+ Dir.mktmpdir do |dir|
1265
+ Dir.chdir(dir) do
1266
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1267
+ FileUtils.mkdir_p('app/models/models')
1268
+
1269
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1270
+
1271
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:images,assets', 'resource_owner:account', 'resource_owner_id:account_id'])
1272
+ allow(instance).to receive(:options).and_return({ engine: nil })
1273
+
1274
+ uploaders = instance.send(:uploaders)
1275
+ expect(uploaders).to all(be_a(String))
1276
+ end
1277
+ end
1278
+ end
1279
+ end
1280
+
1281
+ describe '#is_singular?' do
1282
+ it 'returns true for singular words' do
1283
+ Dir.mktmpdir do |dir|
1284
+ Dir.chdir(dir) do
1285
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1286
+ FileUtils.mkdir_p('app/models/models')
1287
+
1288
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1289
+
1290
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1291
+ allow(instance).to receive(:options).and_return({ engine: nil })
1292
+
1293
+ expect(instance.send(:is_singular?, 'image')).to be true
1294
+ expect(instance.send(:is_singular?, 'asset')).to be true
1295
+ expect(instance.send(:is_singular?, 'picture')).to be true
1296
+ end
1297
+ end
1298
+ end
1299
+
1300
+ it 'returns false for plural words' do
1301
+ Dir.mktmpdir do |dir|
1302
+ Dir.chdir(dir) do
1303
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1304
+ FileUtils.mkdir_p('app/models/models')
1305
+
1306
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1307
+
1308
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1309
+ allow(instance).to receive(:options).and_return({ engine: nil })
1310
+
1311
+ expect(instance.send(:is_singular?, 'images')).to be false
1312
+ expect(instance.send(:is_singular?, 'assets')).to be false
1313
+ expect(instance.send(:is_singular?, 'pictures')).to be false
1314
+ end
1315
+ end
1316
+ end
1317
+
1318
+ it 'handles edge cases like data and media (uncountable)' do
1319
+ Dir.mktmpdir do |dir|
1320
+ Dir.chdir(dir) do
1321
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1322
+ FileUtils.mkdir_p('app/models/models')
1323
+
1324
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1325
+
1326
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1327
+ allow(instance).to receive(:options).and_return({ engine: nil })
1328
+
1329
+ # data and media are uncountable, but ActiveSupport singularize may change them
1330
+ # 'data'.singularize == 'datum', 'media'.singularize == 'medium'
1331
+ # So they are NOT singular according to the method logic
1332
+ expect(instance.send(:is_singular?, 'data')).to be false
1333
+ expect(instance.send(:is_singular?, 'media')).to be false
1334
+ end
1335
+ end
1336
+ end
1337
+ end
1338
+
1339
+ # Priority 2: Edge Cases
1340
+ describe '#columns_meta' do
1341
+ it 'returns all metadata for columns' do
1342
+ Dir.mktmpdir do |dir|
1343
+ Dir.chdir(dir) do
1344
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1345
+ FileUtils.mkdir_p('app/models/models')
1346
+
1347
+ columns = [
1348
+ TestColumn.new('id', :uuid, 'uuid', false, nil, nil, nil, nil),
1349
+ TestColumn.new('title', :string, 'character varying', true, 'default', nil, nil, 255),
1350
+ TestColumn.new('created_at', :datetime, 'timestamp', false, nil, nil, nil, nil),
1351
+ TestColumn.new('updated_at', :datetime, 'timestamp', false, nil, nil, nil, nil)
1352
+ ]
1353
+ create_test_model(class_name: 'Article', columns: columns)
1354
+
1355
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1356
+ allow(instance).to receive(:options).and_return({ engine: nil })
1357
+ instance.send(:setup_variables)
1358
+
1359
+ columns_meta = instance.instance_variable_get(:@columns)
1360
+ title_meta = columns_meta.find { |c| c[:name] == 'title' }
1361
+
1362
+ expect(title_meta).to include(
1363
+ name: 'title',
1364
+ type: :string,
1365
+ sql_type: 'character varying',
1366
+ null: true,
1367
+ default: 'default',
1368
+ limit: 255
1369
+ )
1370
+ end
1371
+ end
1372
+ end
1373
+
1374
+ it 'handles column with sql_type present' do
1375
+ Dir.mktmpdir do |dir|
1376
+ Dir.chdir(dir) do
1377
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1378
+ FileUtils.mkdir_p('app/models/models')
1379
+
1380
+ columns = [
1381
+ TestColumn.new('id', :uuid, 'uuid', false),
1382
+ TestColumn.new('title', :string, 'character varying', true),
1383
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1384
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1385
+ ]
1386
+ create_test_model(class_name: 'Article', columns: columns)
1387
+
1388
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1389
+ allow(instance).to receive(:options).and_return({ engine: nil })
1390
+ instance.send(:setup_variables)
1391
+
1392
+ columns_meta = instance.instance_variable_get(:@columns)
1393
+ title_meta = columns_meta.find { |c| c[:name] == 'title' }
1394
+ expect(title_meta[:sql_type]).to eq('character varying')
1395
+ end
1396
+ end
1397
+ end
1398
+
1399
+ it 'handles column without sql_type' do
1400
+ Dir.mktmpdir do |dir|
1401
+ Dir.chdir(dir) do
1402
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1403
+ FileUtils.mkdir_p('app/models/models')
1404
+
1405
+ column_without_sql_type = Class.new(Struct.new(:name, :type, :null)) do
1406
+ def sql_type
1407
+ nil
1408
+ end
1409
+ end
1410
+
1411
+ columns = [
1412
+ column_without_sql_type.new('id', :uuid, false),
1413
+ TestColumn.new('title', :string, 'character varying', true),
1414
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1415
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1416
+ ]
1417
+ create_test_model(class_name: 'Article', columns: columns)
1418
+
1419
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1420
+ allow(instance).to receive(:options).and_return({ engine: nil })
1421
+ instance.send(:setup_variables)
1422
+
1423
+ columns_meta = instance.instance_variable_get(:@columns)
1424
+ id_meta = columns_meta.find { |c| c[:name] == 'id' }
1425
+ expect(id_meta[:sql_type]).to be_nil
1426
+ end
1427
+ end
1428
+ end
1429
+
1430
+ it 'handles column with null: true and null: false' do
1431
+ Dir.mktmpdir do |dir|
1432
+ Dir.chdir(dir) do
1433
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1434
+ FileUtils.mkdir_p('app/models/models')
1435
+
1436
+ columns = [
1437
+ TestColumn.new('id', :uuid, 'uuid', false),
1438
+ TestColumn.new('title', :string, 'character varying', true),
1439
+ TestColumn.new('required_field', :string, 'character varying', false),
1440
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1441
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1442
+ ]
1443
+ create_test_model(class_name: 'Article', columns: columns)
1444
+
1445
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1446
+ allow(instance).to receive(:options).and_return({ engine: nil })
1447
+ instance.send(:setup_variables)
1448
+
1449
+ columns_meta = instance.instance_variable_get(:@columns)
1450
+ title_meta = columns_meta.find { |c| c[:name] == 'title' }
1451
+ required_meta = columns_meta.find { |c| c[:name] == 'required_field' }
1452
+
1453
+ expect(title_meta[:null]).to be true
1454
+ expect(required_meta[:null]).to be false
1455
+ end
1456
+ end
1457
+ end
1458
+
1459
+ it 'handles column with default value' do
1460
+ Dir.mktmpdir do |dir|
1461
+ Dir.chdir(dir) do
1462
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1463
+ FileUtils.mkdir_p('app/models/models')
1464
+
1465
+ columns = [
1466
+ TestColumn.new('id', :uuid, 'uuid', false),
1467
+ TestColumn.new('title', :string, 'character varying', true, 'Untitled'),
1468
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1469
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1470
+ ]
1471
+ create_test_model(class_name: 'Article', columns: columns)
1472
+
1473
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1474
+ allow(instance).to receive(:options).and_return({ engine: nil })
1475
+ instance.send(:setup_variables)
1476
+
1477
+ columns_meta = instance.instance_variable_get(:@columns)
1478
+ title_meta = columns_meta.find { |c| c[:name] == 'title' }
1479
+ expect(title_meta[:default]).to eq('Untitled')
1480
+ end
1481
+ end
1482
+ end
1483
+
1484
+ it 'handles decimal column with precision and scale' do
1485
+ Dir.mktmpdir do |dir|
1486
+ Dir.chdir(dir) do
1487
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1488
+ FileUtils.mkdir_p('app/models/models')
1489
+
1490
+ columns = [
1491
+ TestColumn.new('id', :uuid, 'uuid', false),
1492
+ TestColumn.new('price', :decimal, 'decimal', true, nil, 10, 2, nil),
1493
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1494
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1495
+ ]
1496
+ create_test_model(class_name: 'Article', columns: columns)
1497
+
1498
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1499
+ allow(instance).to receive(:options).and_return({ engine: nil })
1500
+ instance.send(:setup_variables)
1501
+
1502
+ columns_meta = instance.instance_variable_get(:@columns)
1503
+ price_meta = columns_meta.find { |c| c[:name] == 'price' }
1504
+ expect(price_meta[:precision]).to eq(10)
1505
+ expect(price_meta[:scale]).to eq(2)
1506
+ end
1507
+ end
1508
+ end
1509
+
1510
+ it 'handles column with limit' do
1511
+ Dir.mktmpdir do |dir|
1512
+ Dir.chdir(dir) do
1513
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1514
+ FileUtils.mkdir_p('app/models/models')
1515
+
1516
+ columns = [
1517
+ TestColumn.new('id', :uuid, 'uuid', false),
1518
+ TestColumn.new('title', :string, 'character varying', true, nil, nil, nil, 255),
1519
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1520
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1521
+ ]
1522
+ create_test_model(class_name: 'Article', columns: columns)
1523
+
1524
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1525
+ allow(instance).to receive(:options).and_return({ engine: nil })
1526
+ instance.send(:setup_variables)
1527
+
1528
+ columns_meta = instance.instance_variable_get(:@columns)
1529
+ title_meta = columns_meta.find { |c| c[:name] == 'title' }
1530
+ expect(title_meta[:limit]).to eq(255)
1531
+ end
1532
+ end
1533
+ end
1534
+
1535
+ it 'checks respond_to? for optional methods' do
1536
+ Dir.mktmpdir do |dir|
1537
+ Dir.chdir(dir) do
1538
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1539
+ FileUtils.mkdir_p('app/models/models')
1540
+
1541
+ # Create a column that doesn't respond to all methods
1542
+ minimal_column = Struct.new(:name, :type) do
1543
+ def to_s
1544
+ name.to_s
1545
+ end
1546
+ end
1547
+
1548
+ columns = [
1549
+ minimal_column.new('id', :uuid),
1550
+ TestColumn.new('title', :string, 'character varying', true),
1551
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
1552
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
1553
+ ]
1554
+ create_test_model(class_name: 'Article', columns: columns)
1555
+
1556
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1557
+ allow(instance).to receive(:options).and_return({ engine: nil })
1558
+ instance.send(:setup_variables)
1559
+
1560
+ columns_meta = instance.instance_variable_get(:@columns)
1561
+ id_meta = columns_meta.find { |c| c[:name] == 'id' }
1562
+ expect(id_meta[:sql_type]).to be_nil
1563
+ expect(id_meta[:null]).to be_nil
1564
+ end
1565
+ end
1566
+ end
1567
+
1568
+ it 'returns array of hashes' do
1569
+ Dir.mktmpdir do |dir|
1570
+ Dir.chdir(dir) do
1571
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1572
+ FileUtils.mkdir_p('app/models/models')
1573
+
1574
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1575
+
1576
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1577
+ allow(instance).to receive(:options).and_return({ engine: nil })
1578
+ instance.send(:setup_variables)
1579
+
1580
+ columns_meta = instance.instance_variable_get(:@columns)
1581
+ expect(columns_meta).to be_an(Array)
1582
+ expect(columns_meta.all? { |c| c.is_a?(Hash) }).to be true
1583
+ end
1584
+ end
1585
+ end
1586
+ end
1587
+
1588
+ describe 'getter methods' do
1589
+ it 'returns cached value for contract_lines_for_create' do
1590
+ Dir.mktmpdir do |dir|
1591
+ Dir.chdir(dir) do
1592
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1593
+ FileUtils.mkdir_p('app/models/models')
1594
+
1595
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1596
+
1597
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1598
+ allow(instance).to receive(:options).and_return({ engine: nil })
1599
+ instance.send(:setup_variables)
1600
+
1601
+ cached_value = instance.instance_variable_get(:@contract_lines_for_create)
1602
+ # Getter method is private, so we need to use send
1603
+ expect(instance.send(:contract_lines_for_create)).to eq(cached_value)
1604
+ end
1605
+ end
1606
+ end
1607
+
1608
+ it 'returns empty array for contract_lines_for_create when not set' do
1609
+ Dir.mktmpdir do |dir|
1610
+ Dir.chdir(dir) do
1611
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1612
+ FileUtils.mkdir_p('app/models/models')
1613
+
1614
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1615
+
1616
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1617
+ allow(instance).to receive(:options).and_return({ engine: nil })
1618
+ instance.instance_variable_set(:@contract_lines_for_create, nil)
1619
+
1620
+ expect(instance.send(:contract_lines_for_create)).to eq([])
1621
+ end
1622
+ end
1623
+ end
1624
+
1625
+ it 'returns cached value for contract_lines_for_update' do
1626
+ Dir.mktmpdir do |dir|
1627
+ Dir.chdir(dir) do
1628
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1629
+ FileUtils.mkdir_p('app/models/models')
1630
+
1631
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1632
+
1633
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1634
+ allow(instance).to receive(:options).and_return({ engine: nil })
1635
+ instance.send(:setup_variables)
1636
+
1637
+ cached_value = instance.instance_variable_get(:@contract_lines_for_update)
1638
+ expect(instance.send(:contract_lines_for_update)).to eq(cached_value)
1639
+ end
1640
+ end
1641
+ end
1642
+
1643
+ it 'returns empty array for contract_lines_for_update when not set' do
1644
+ Dir.mktmpdir do |dir|
1645
+ Dir.chdir(dir) do
1646
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1647
+ FileUtils.mkdir_p('app/models/models')
1648
+
1649
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1650
+
1651
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1652
+ allow(instance).to receive(:options).and_return({ engine: nil })
1653
+ instance.instance_variable_set(:@contract_lines_for_update, nil)
1654
+
1655
+ expect(instance.send(:contract_lines_for_update)).to eq([])
1656
+ end
1657
+ end
1658
+ end
1659
+
1660
+ it 'returns cached value for contract_lines_for_list' do
1661
+ Dir.mktmpdir do |dir|
1662
+ Dir.chdir(dir) do
1663
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1664
+ FileUtils.mkdir_p('app/models/models')
1665
+
1666
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1667
+
1668
+ instance = klass.new(['Models::Article', 'actor:user', 'search_able:title', 'resource_owner:account', 'resource_owner_id:account_id'])
1669
+ allow(instance).to receive(:options).and_return({ engine: nil })
1670
+ instance.send(:setup_variables)
1671
+
1672
+ cached_value = instance.instance_variable_get(:@contract_lines_for_list)
1673
+ expect(instance.send(:contract_lines_for_list)).to eq(cached_value)
1674
+ end
1675
+ end
1676
+ end
1677
+
1678
+ it 'returns empty array for contract_lines_for_list when not set' do
1679
+ Dir.mktmpdir do |dir|
1680
+ Dir.chdir(dir) do
1681
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1682
+ FileUtils.mkdir_p('app/models/models')
1683
+
1684
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1685
+
1686
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1687
+ allow(instance).to receive(:options).and_return({ engine: nil })
1688
+ instance.instance_variable_set(:@contract_lines_for_list, nil)
1689
+
1690
+ expect(instance.send(:contract_lines_for_list)).to eq([])
1691
+ end
1692
+ end
1693
+ end
1694
+
1695
+ it 'returns cached value for repository_list_filters' do
1696
+ Dir.mktmpdir do |dir|
1697
+ Dir.chdir(dir) do
1698
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1699
+ FileUtils.mkdir_p('app/models/models')
1700
+
1701
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1702
+
1703
+ instance = klass.new(['Models::Article', 'actor:user', 'search_able:title', 'resource_owner:account', 'resource_owner_id:account_id'])
1704
+ allow(instance).to receive(:options).and_return({ engine: nil })
1705
+ instance.send(:setup_variables)
1706
+
1707
+ cached_value = instance.instance_variable_get(:@repository_list_filters)
1708
+ expect(instance.send(:repository_list_filters)).to eq(cached_value)
1709
+ end
1710
+ end
1711
+ end
1712
+
1713
+ it 'returns empty array for repository_list_filters when not set' do
1714
+ Dir.mktmpdir do |dir|
1715
+ Dir.chdir(dir) do
1716
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1717
+ FileUtils.mkdir_p('app/models/models')
1718
+
1719
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1720
+
1721
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1722
+ allow(instance).to receive(:options).and_return({ engine: nil })
1723
+ instance.instance_variable_set(:@repository_list_filters, nil)
1724
+
1725
+ expect(instance.send(:repository_list_filters)).to eq([])
1726
+ end
1727
+ end
1728
+ end
1729
+
1730
+ it 'returns cached value for entity_db_fields' do
1731
+ Dir.mktmpdir do |dir|
1732
+ Dir.chdir(dir) do
1733
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1734
+ FileUtils.mkdir_p('app/models/models')
1735
+
1736
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1737
+
1738
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1739
+ allow(instance).to receive(:options).and_return({ engine: nil })
1740
+ instance.send(:setup_variables)
1741
+
1742
+ cached_value = instance.instance_variable_get(:@entity_db_fields)
1743
+ expect(instance.send(:entity_db_fields)).to eq(cached_value)
1744
+ end
1745
+ end
1746
+ end
1747
+
1748
+ it 'returns cached value for entity_uploader_definitions' do
1749
+ Dir.mktmpdir do |dir|
1750
+ Dir.chdir(dir) do
1751
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1752
+ FileUtils.mkdir_p('app/models/models')
1753
+
1754
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1755
+
1756
+ instance = klass.new(['Models::Article', 'actor:user', 'uploaders:image', 'resource_owner:account', 'resource_owner_id:account_id'])
1757
+ allow(instance).to receive(:options).and_return({ engine: nil })
1758
+ instance.send(:setup_variables)
1759
+
1760
+ cached_value = instance.instance_variable_get(:@entity_uploader_definitions)
1761
+ expect(instance.send(:entity_uploader_definitions)).to eq(cached_value)
1762
+ end
1763
+ end
1764
+ end
1765
+
1766
+ it 'returns empty array for entity_uploader_definitions when not set' do
1767
+ Dir.mktmpdir do |dir|
1768
+ Dir.chdir(dir) do
1769
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1770
+ FileUtils.mkdir_p('app/models/models')
1771
+
1772
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1773
+
1774
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1775
+ allow(instance).to receive(:options).and_return({ engine: nil })
1776
+ instance.instance_variable_set(:@entity_uploader_definitions, nil)
1777
+
1778
+ expect(instance.send(:entity_uploader_definitions)).to eq([])
1779
+ end
1780
+ end
1781
+ end
1782
+ end
1783
+
1784
+ describe '#configure_engine' do
1785
+ it 'handles combination of --engine and --domain together' do
1786
+ Dir.mktmpdir do |dir|
1787
+ Dir.chdir(dir) do
1788
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1789
+ FileUtils.mkdir_p('app/models/models')
1790
+
1791
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1792
+
1793
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1794
+ allow(instance).to receive(:options).and_return({ engine: 'Core', domain: 'admin/' })
1795
+ instance.send(:configure_engine)
1796
+
1797
+ expect(RiderKick.configuration.engine_name).to eq('Core')
1798
+ expect(RiderKick.configuration.domain_scope).to eq('core/admin/')
1799
+ end
1800
+ end
1801
+ end
1802
+
1803
+ it 'handles --domain with trailing slash' do
1804
+ Dir.mktmpdir do |dir|
1805
+ Dir.chdir(dir) do
1806
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1807
+ FileUtils.mkdir_p('app/models/models')
1808
+
1809
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1810
+
1811
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1812
+ allow(instance).to receive(:options).and_return({ engine: nil, domain: 'admin/' })
1813
+ instance.send(:configure_engine)
1814
+
1815
+ expect(RiderKick.configuration.domain_scope).to eq('admin/')
1816
+ end
1817
+ end
1818
+ end
1819
+
1820
+ it 'handles --domain without trailing slash' do
1821
+ Dir.mktmpdir do |dir|
1822
+ Dir.chdir(dir) do
1823
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1824
+ FileUtils.mkdir_p('app/models/models')
1825
+
1826
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1827
+
1828
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1829
+ allow(instance).to receive(:options).and_return({ engine: nil, domain: 'admin' })
1830
+ instance.send(:configure_engine)
1831
+
1832
+ expect(RiderKick.configuration.domain_scope).to eq('admin')
1833
+ end
1834
+ end
1835
+ end
1836
+
1837
+ it 'prevents duplicate messages with @engine_configured flag' do
1838
+ Dir.mktmpdir do |dir|
1839
+ Dir.chdir(dir) do
1840
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1841
+ FileUtils.mkdir_p('app/models/models')
1842
+
1843
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1844
+
1845
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1846
+ allow(instance).to receive(:options).and_return({ engine: nil })
1847
+
1848
+ # First call should show message
1849
+ expect(instance).to receive(:say).at_least(:once)
1850
+ instance.send(:configure_engine)
1851
+
1852
+ # Set flag and call again - should not show message again
1853
+ instance.instance_variable_set(:@engine_configured, true)
1854
+ expect(instance).not_to receive(:say)
1855
+ instance.send(:configure_engine)
1856
+ end
1857
+ end
1858
+ end
1859
+
1860
+ it 'handles engine name with underscore' do
1861
+ Dir.mktmpdir do |dir|
1862
+ Dir.chdir(dir) do
1863
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1864
+ FileUtils.mkdir_p('app/models/models')
1865
+
1866
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1867
+
1868
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1869
+ allow(instance).to receive(:options).and_return({ engine: 'order_engine', domain: nil })
1870
+ instance.send(:configure_engine)
1871
+
1872
+ expect(RiderKick.configuration.engine_name).to eq('order_engine')
1873
+ expect(RiderKick.configuration.domain_scope).to eq('order_engine/')
1874
+ end
1875
+ end
1876
+ end
1877
+
1878
+ it 'handles engine name with camelCase' do
1879
+ Dir.mktmpdir do |dir|
1880
+ Dir.chdir(dir) do
1881
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1882
+ FileUtils.mkdir_p('app/models/models')
1883
+
1884
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1885
+
1886
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1887
+ allow(instance).to receive(:options).and_return({ engine: 'OrderEngine', domain: nil })
1888
+ instance.send(:configure_engine)
1889
+
1890
+ expect(RiderKick.configuration.engine_name).to eq('OrderEngine')
1891
+ expect(RiderKick.configuration.domain_scope).to eq('order_engine/')
1892
+ end
1893
+ end
1894
+ end
1895
+
1896
+ it 'concatenates domain scope when engine + domain' do
1897
+ Dir.mktmpdir do |dir|
1898
+ Dir.chdir(dir) do
1899
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1900
+ FileUtils.mkdir_p('app/models/models')
1901
+
1902
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1903
+
1904
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1905
+ allow(instance).to receive(:options).and_return({ engine: 'Core', domain: 'admin/' })
1906
+ instance.send(:configure_engine)
1907
+
1908
+ expect(RiderKick.configuration.domain_scope).to eq('core/admin/')
1909
+ end
1910
+ end
1911
+ end
1912
+
1913
+ it 'sets domain scope when only domain (without engine)' do
1914
+ Dir.mktmpdir do |dir|
1915
+ Dir.chdir(dir) do
1916
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1917
+ FileUtils.mkdir_p('app/models/models')
1918
+
1919
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1920
+
1921
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
1922
+ allow(instance).to receive(:options).and_return({ engine: nil, domain: 'admin/' })
1923
+ instance.send(:configure_engine)
1924
+
1925
+ expect(RiderKick.configuration.domain_scope).to eq('admin/')
1926
+ end
1927
+ end
1928
+ end
1929
+ end
1930
+
1931
+ describe '#validation!' do
1932
+ it 'trims whitespace from actor' do
1933
+ Dir.mktmpdir do |dir|
1934
+ Dir.chdir(dir) do
1935
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1936
+ FileUtils.mkdir_p('app/models/models')
1937
+
1938
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1939
+
1940
+ instance = klass.new(['Models::Article', 'actor: user ', 'resource_owner:account', 'resource_owner_id:account_id'])
1941
+ allow(instance).to receive(:options).and_return({ engine: nil })
1942
+
1943
+ expect { instance.send(:validation!) }.not_to raise_error
1944
+ end
1945
+ end
1946
+ end
1947
+
1948
+ it 'trims whitespace from resource_owner' do
1949
+ Dir.mktmpdir do |dir|
1950
+ Dir.chdir(dir) do
1951
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1952
+ FileUtils.mkdir_p('app/models/models')
1953
+
1954
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1955
+
1956
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner: account ', 'resource_owner_id:account_id'])
1957
+ allow(instance).to receive(:options).and_return({ engine: nil })
1958
+
1959
+ expect { instance.send(:validation!) }.not_to raise_error
1960
+ end
1961
+ end
1962
+ end
1963
+
1964
+ it 'trims whitespace from resource_owner_id' do
1965
+ Dir.mktmpdir do |dir|
1966
+ Dir.chdir(dir) do
1967
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1968
+ FileUtils.mkdir_p('app/models/models')
1969
+
1970
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1971
+
1972
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id: account_id '])
1973
+ allow(instance).to receive(:options).and_return({ engine: nil })
1974
+
1975
+ expect { instance.send(:validation!) }.not_to raise_error
1976
+ end
1977
+ end
1978
+ end
1979
+
1980
+ it 'raises error with clear message for missing actor' do
1981
+ Dir.mktmpdir do |dir|
1982
+ Dir.chdir(dir) do
1983
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
1984
+ FileUtils.mkdir_p('app/models/models')
1985
+
1986
+ create_test_model(class_name: 'Article', columns: create_test_columns)
1987
+
1988
+ instance = klass.new(['Models::Article', 'resource_owner:account', 'resource_owner_id:account_id'])
1989
+ allow(instance).to receive(:options).and_return({ engine: nil })
1990
+
1991
+ expect {
1992
+ instance.send(:validation!)
1993
+ }.to raise_error(RiderKick::ValidationError, /Missing required setting: actor/)
1994
+ end
1995
+ end
1996
+ end
1997
+
1998
+ it 'raises error with clear message for missing resource_owner' do
1999
+ Dir.mktmpdir do |dir|
2000
+ Dir.chdir(dir) do
2001
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
2002
+ FileUtils.mkdir_p('app/models/models')
2003
+
2004
+ create_test_model(class_name: 'Article', columns: create_test_columns)
2005
+
2006
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner_id:account_id'])
2007
+ allow(instance).to receive(:options).and_return({ engine: nil })
2008
+
2009
+ expect {
2010
+ instance.send(:validation!)
2011
+ }.to raise_error(RiderKick::ValidationError, /Missing required setting: resource_owner/)
2012
+ end
2013
+ end
2014
+ end
2015
+
2016
+ it 'raises error with clear message for missing resource_owner_id' do
2017
+ Dir.mktmpdir do |dir|
2018
+ Dir.chdir(dir) do
2019
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
2020
+ FileUtils.mkdir_p('app/models/models')
2021
+
2022
+ create_test_model(class_name: 'Article', columns: create_test_columns)
2023
+
2024
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account'])
2025
+ allow(instance).to receive(:options).and_return({ engine: nil })
2026
+
2027
+ expect {
2028
+ instance.send(:validation!)
2029
+ }.to raise_error(RiderKick::ValidationError, /Missing required setting: resource_owner_id/)
2030
+ end
2031
+ end
2032
+ end
2033
+
2034
+ it 'raises error when domains_path does not exist' do
2035
+ Dir.mktmpdir do |dir|
2036
+ Dir.chdir(dir) do
2037
+ FileUtils.mkdir_p('app/models/models')
2038
+
2039
+ create_test_model(class_name: 'Article', columns: create_test_columns)
2040
+
2041
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
2042
+ allow(instance).to receive(:options).and_return({ engine: nil })
2043
+
2044
+ expect {
2045
+ instance.send(:validation!)
2046
+ }.to raise_error(RiderKick::ValidationError, /clean_arch.*--setup/i)
2047
+ end
2048
+ end
2049
+ end
2050
+ end
2051
+
2052
+ # Priority 3: Meta Methods
2053
+ describe '#fkeys_meta' do
2054
+ it 'returns empty array' do
2055
+ Dir.mktmpdir do |dir|
2056
+ Dir.chdir(dir) do
2057
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
2058
+ FileUtils.mkdir_p('app/models/models')
2059
+
2060
+ create_test_model(class_name: 'Article', columns: create_test_columns)
2061
+
2062
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
2063
+ allow(instance).to receive(:options).and_return({ engine: nil })
2064
+
2065
+ expect(instance.send(:fkeys_meta)).to eq([])
2066
+ end
2067
+ end
2068
+ end
2069
+ end
2070
+
2071
+ describe '#indexes_meta' do
2072
+ it 'returns empty array' do
2073
+ Dir.mktmpdir do |dir|
2074
+ Dir.chdir(dir) do
2075
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
2076
+ FileUtils.mkdir_p('app/models/models')
2077
+
2078
+ create_test_model(class_name: 'Article', columns: create_test_columns)
2079
+
2080
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
2081
+ allow(instance).to receive(:options).and_return({ engine: nil })
2082
+
2083
+ expect(instance.send(:indexes_meta)).to eq([])
2084
+ end
2085
+ end
2086
+ end
2087
+ end
2088
+
2089
+ describe '#enums_meta' do
2090
+ it 'returns empty hash' do
2091
+ Dir.mktmpdir do |dir|
2092
+ Dir.chdir(dir) do
2093
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
2094
+ FileUtils.mkdir_p('app/models/models')
2095
+
2096
+ create_test_model(class_name: 'Article', columns: create_test_columns)
2097
+
2098
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
2099
+ allow(instance).to receive(:options).and_return({ engine: nil })
2100
+
2101
+ expect(instance.send(:enums_meta)).to eq({})
2102
+ end
2103
+ end
2104
+ end
2105
+ end
2106
+
2107
+ describe '#generate_files' do
2108
+ it 'generates path for engine' do
2109
+ Dir.mktmpdir do |dir|
2110
+ Dir.chdir(dir) do
2111
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
2112
+ FileUtils.mkdir_p('app/models/models')
2113
+ FileUtils.mkdir_p('engines/core/db/structures')
2114
+
2115
+ create_test_model(class_name: 'Article', columns: create_test_columns)
2116
+
2117
+ RiderKick.configuration.engine_name = 'Core'
2118
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
2119
+ allow(instance).to receive(:options).and_return({ engine: nil })
2120
+ instance.send(:setup_variables)
2121
+
2122
+ expect(instance).to receive(:template).with(
2123
+ 'db/structures/example.yaml.tt',
2124
+ 'engines/core/db/structures/articles_structure.yaml'
2125
+ )
2126
+ instance.send(:generate_files, 'articles')
2127
+ end
2128
+ end
2129
+ end
2130
+
2131
+ it 'generates path for main app' do
2132
+ Dir.mktmpdir do |dir|
2133
+ Dir.chdir(dir) do
2134
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
2135
+ FileUtils.mkdir_p('app/models/models')
2136
+ FileUtils.mkdir_p('db/structures')
2137
+
2138
+ create_test_model(class_name: 'Article', columns: create_test_columns)
2139
+
2140
+ RiderKick.configuration.engine_name = nil
2141
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
2142
+ allow(instance).to receive(:options).and_return({ engine: nil })
2143
+ instance.send(:setup_variables)
2144
+
2145
+ expect(instance).to receive(:template).with(
2146
+ 'db/structures/example.yaml.tt',
2147
+ 'db/structures/articles_structure.yaml'
2148
+ )
2149
+ instance.send(:generate_files, 'articles')
2150
+ end
2151
+ end
2152
+ end
2153
+ end
2154
+
2155
+ # Priority 4: Error Handling
2156
+ describe 'error handling' do
2157
+ it 'handles constantize error when model class not found' do
2158
+ Dir.mktmpdir do |dir|
2159
+ Dir.chdir(dir) do
2160
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
2161
+ FileUtils.mkdir_p('app/models/models')
2162
+
2163
+ instance = klass.new(['Models::NonExistent', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
2164
+ allow(instance).to receive(:options).and_return({ engine: nil })
2165
+
2166
+ expect {
2167
+ instance.send(:setup_variables)
2168
+ }.to raise_error(RiderKick::ModelNotFoundError)
2169
+ end
2170
+ end
2171
+ end
2172
+
2173
+ it 'handles nil columns_hash[field] in get_column_type' do
2174
+ Dir.mktmpdir do |dir|
2175
+ Dir.chdir(dir) do
2176
+ FileUtils.mkdir_p(RiderKick.configuration.domains_path + '/use_cases')
2177
+ FileUtils.mkdir_p('app/models/models')
2178
+
2179
+ columns = [
2180
+ TestColumn.new('id', :uuid, 'uuid', false),
2181
+ TestColumn.new('title', :string, 'character varying', true),
2182
+ TestColumn.new('created_at', :datetime, 'timestamp', false),
2183
+ TestColumn.new('updated_at', :datetime, 'timestamp', false)
2184
+ ]
2185
+ create_test_model(class_name: 'Article', columns: columns)
2186
+
2187
+ instance = klass.new(['Models::Article', 'actor:user', 'resource_owner:account', 'resource_owner_id:account_id'])
2188
+ allow(instance).to receive(:options).and_return({ engine: nil })
2189
+ instance.send(:setup_variables)
2190
+
2191
+ # Mock columns_hash to return nil for a field
2192
+ model_class = instance.instance_variable_get(:@model_class)
2193
+ allow(model_class).to receive(:columns_hash).and_return({ 'title' => nil })
2194
+
2195
+ expect {
2196
+ instance.send(:get_column_type, 'nonexistent')
2197
+ }.to raise_error(NoMethodError)
2198
+ end
2199
+ end
2200
+ end
2201
+ end
2202
+ end