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,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'active_support/inflector'
5
+ require 'faker'
6
+ require_relative 'base_generator'
7
+ require_relative '../../rider-kick'
8
+
9
+ module RiderKick
10
+ class FactoryGenerator < BaseGenerator
11
+ source_root File.expand_path('templates', __dir__)
12
+
13
+ argument :arg_model_name, type: :string, banner: 'Models::Article'
14
+ argument :arg_scope, type: :hash, default: {}, banner: 'scope:core'
15
+ class_option :engine, type: :string, default: nil, desc: 'Specify engine name (e.g., Core, Admin)'
16
+ class_option :static, type: :boolean, default: false, desc: 'Generate static values instead of Faker calls'
17
+
18
+ def generate_factory
19
+ configure_engine
20
+ setup_variables
21
+ generate_factory_file
22
+ end
23
+
24
+ private
25
+
26
+ def setup_variables
27
+ @model_name = arg_model_name
28
+ validate_model_exists!(@model_name)
29
+ @model_class = @model_name.constantize
30
+ @variable_subject = @model_name.split('::').last.underscore.downcase
31
+ @factory_name = @variable_subject
32
+ @scope_path = arg_scope.fetch('scope', '').to_s.downcase
33
+
34
+ # Get all columns excluding those we want to skip
35
+ @attributes = @model_class.columns.reject do |column|
36
+ skip_column?(column.name)
37
+ end
38
+ rescue ModelNotFoundError => e
39
+ say "Error: #{e.message}", :red
40
+ raise
41
+ end
42
+
43
+ def skip_column?(column_name)
44
+ # Skip id, timestamps, and all foreign key columns (ending with _id)
45
+ ['id', 'created_at', 'updated_at', 'type'].include?(column_name) ||
46
+ column_name.end_with?('_id')
47
+ end
48
+
49
+ def generate_factory_file
50
+ factory_dir = if @scope_path.present?
51
+ File.join('spec', 'factories', @scope_path)
52
+ else
53
+ File.join('spec', 'factories')
54
+ end
55
+
56
+ empty_directory factory_dir unless Dir.exist?(factory_dir)
57
+
58
+ factory_file_path = File.join(factory_dir, "#{@factory_name}.rb")
59
+ template 'spec/factories/factory.rb.tt', factory_file_path
60
+
61
+ say "Factory created: #{factory_file_path}", :green
62
+ end
63
+
64
+ def generate_faker_value(column)
65
+ faker_expression = get_faker_expression(column)
66
+
67
+ # Time-based columns always use Time.zone.now, even with --static
68
+ if is_time_column?(column)
69
+ faker_expression
70
+ elsif options[:static]
71
+ evaluate_faker_expression(faker_expression)
72
+ else
73
+ faker_expression
74
+ end
75
+ end
76
+
77
+ def is_time_column?(column)
78
+ ['datetime', 'timestamp', 'time'].include?(column.type.to_s)
79
+ end
80
+
81
+ def get_faker_expression(column)
82
+ case column.type.to_s
83
+ when 'string'
84
+ if column.name.include?('email')
85
+ 'Faker::Internet.email'
86
+ elsif column.name.include?('name')
87
+ 'Faker::Name.name'
88
+ elsif column.name.include?('phone')
89
+ 'Faker::PhoneNumber.phone_number'
90
+ elsif column.name.include?('address')
91
+ 'Faker::Address.full_address'
92
+ elsif column.name.include?('city')
93
+ 'Faker::Address.city'
94
+ elsif column.name.include?('country')
95
+ 'Faker::Address.country'
96
+ elsif column.name.include?('url') || column.name.include?('website')
97
+ 'Faker::Internet.url'
98
+ elsif column.name.include?('title')
99
+ 'Faker::Lorem.sentence(word_count: 3)'
100
+ elsif column.name.include?('code')
101
+ 'Faker::Alphanumeric.alphanumeric(number: 10)'
102
+ else
103
+ 'Faker::Lorem.word'
104
+ end
105
+ when 'text'
106
+ if column.name.include?('description') || column.name.include?('content') || column.name.include?('body')
107
+ 'Faker::Lorem.paragraph(sentence_count: 3)'
108
+ else
109
+ 'Faker::Lorem.sentence'
110
+ end
111
+ when 'integer'
112
+ if column.name.include?('count') || column.name.include?('quantity')
113
+ 'Faker::Number.between(from: 1, to: 100)'
114
+ elsif column.name.include?('age')
115
+ 'Faker::Number.between(from: 18, to: 80)'
116
+ elsif column.name.include?('price') || column.name.include?('amount')
117
+ 'Faker::Number.between(from: 1000, to: 1000000)'
118
+ else
119
+ 'Faker::Number.number(digits: 5)'
120
+ end
121
+ when 'bigint'
122
+ 'Faker::Number.number(digits: 10)'
123
+ when 'float'
124
+ 'Faker::Number.decimal(l_digits: 2, r_digits: 2)'
125
+ when 'decimal'
126
+ if column.name.include?('price') || column.name.include?('amount')
127
+ 'Faker::Commerce.price'
128
+ else
129
+ 'Faker::Number.decimal(l_digits: 4, r_digits: 2)'
130
+ end
131
+ when 'boolean'
132
+ '[true, false].sample'
133
+ when 'date'
134
+ 'Faker::Date.between(from: 1.year.ago, to: Date.today)'
135
+ when 'datetime', 'timestamp', 'time'
136
+ 'Time.zone.now'
137
+ when 'uuid'
138
+ 'SecureRandom.uuid'
139
+ when 'json', 'jsonb'
140
+ '{ key: Faker::Lorem.word, value: Faker::Lorem.sentence }'
141
+ when 'inet'
142
+ 'Faker::Internet.ip_v4_address'
143
+ when 'cidr'
144
+ 'Faker::Internet.ip_v4_cidr'
145
+ when 'macaddr'
146
+ 'Faker::Internet.mac_address'
147
+ else
148
+ # Default fallback
149
+ 'Faker::Lorem.word'
150
+ end
151
+ end
152
+
153
+ def evaluate_faker_expression(expression)
154
+ # Safely evaluate the Faker expression using whitelist mapping
155
+ result = safe_evaluate_faker_expression(expression)
156
+ format_static_value(result)
157
+ rescue => e
158
+ say "Warning: Could not evaluate '#{expression}': #{e.message}", :yellow
159
+ expression # Fallback to original expression if evaluation fails
160
+ end
161
+
162
+ def safe_evaluate_faker_expression(expression)
163
+ # Check custom FakerMapping registry first
164
+ custom_mapping = RiderKick::FakerMapping.get(expression)
165
+ return custom_mapping.call if custom_mapping
166
+
167
+ # Whitelist mapping for safe Faker expression evaluation
168
+ faker_methods = {
169
+ 'Faker::Internet.email' => -> { Faker::Internet.email },
170
+ 'Faker::Name.name' => -> { Faker::Name.name },
171
+ 'Faker::PhoneNumber.phone_number' => -> { Faker::PhoneNumber.phone_number },
172
+ 'Faker::Address.full_address' => -> { Faker::Address.full_address },
173
+ 'Faker::Address.city' => -> { Faker::Address.city },
174
+ 'Faker::Address.country' => -> { Faker::Address.country },
175
+ 'Faker::Internet.url' => -> { Faker::Internet.url },
176
+ 'Faker::Lorem.sentence(word_count: 3)' => -> { Faker::Lorem.sentence(word_count: 3) },
177
+ 'Faker::Alphanumeric.alphanumeric(number: 10)' => -> { Faker::Alphanumeric.alphanumeric(number: 10) },
178
+ 'Faker::Lorem.word' => -> { Faker::Lorem.word },
179
+ 'Faker::Lorem.paragraph(sentence_count: 3)' => -> { Faker::Lorem.paragraph(sentence_count: 3) },
180
+ 'Faker::Lorem.sentence' => -> { Faker::Lorem.sentence },
181
+ 'Faker::Number.between(from: 1, to: 100)' => -> { Faker::Number.between(from: 1, to: 100) },
182
+ 'Faker::Number.between(from: 18, to: 80)' => -> { Faker::Number.between(from: 18, to: 80) },
183
+ 'Faker::Number.between(from: 1000, to: 1000000)' => -> { Faker::Number.between(from: 1000, to: 1_000_000) },
184
+ 'Faker::Number.number(digits: 5)' => -> { Faker::Number.number(digits: 5) },
185
+ 'Faker::Number.number(digits: 10)' => -> { Faker::Number.number(digits: 10) },
186
+ 'Faker::Number.decimal(l_digits: 2, r_digits: 2)' => -> { Faker::Number.decimal(l_digits: 2, r_digits: 2) },
187
+ 'Faker::Commerce.price' => -> { Faker::Commerce.price },
188
+ 'Faker::Number.decimal(l_digits: 4, r_digits: 2)' => -> { Faker::Number.decimal(l_digits: 4, r_digits: 2) },
189
+ '[true, false].sample' => -> { [true, false].sample },
190
+ 'Faker::Date.between(from: 1.year.ago, to: Date.today)' => -> { Faker::Date.between(from: 1.year.ago, to: Date.today) },
191
+ 'Time.zone.now' => -> { Time.zone.now },
192
+ 'SecureRandom.uuid' => -> { SecureRandom.uuid },
193
+ '{ key: Faker::Lorem.word, value: Faker::Lorem.sentence }' => -> { { key: Faker::Lorem.word, value: Faker::Lorem.sentence } },
194
+ 'Faker::Internet.ip_v4_address' => -> { Faker::Internet.ip_v4_address },
195
+ 'Faker::Internet.ip_v4_cidr' => -> { Faker::Internet.ip_v4_cidr },
196
+ 'Faker::Internet.mac_address' => -> { Faker::Internet.mac_address }
197
+ }
198
+
199
+ # Check if expression is in whitelist
200
+ if faker_methods.key?(expression)
201
+ faker_methods[expression].call
202
+ else
203
+ # Fallback: try to parse simple expressions
204
+ # Handle expressions with parameters (basic parsing)
205
+ case expression
206
+ when /^Faker::(\w+)\.(\w+)$/
207
+ # Simple Faker::Module.method format
208
+ module_name = Regexp.last_match(1)
209
+ method_name = Regexp.last_match(2)
210
+ faker_module = Faker.const_get(module_name)
211
+ faker_module.public_send(method_name)
212
+ else
213
+ # If not in whitelist and can't parse, raise error
214
+ raise ArgumentError, "Expression '#{expression}' is not in the safe whitelist"
215
+ end
216
+ end
217
+ end
218
+
219
+ def format_static_value(value)
220
+ case value
221
+ when String
222
+ "'#{value.gsub("'", "\\\\'")}'"
223
+ when Numeric
224
+ value.to_s
225
+ when TrueClass, FalseClass
226
+ value.to_s
227
+ when Date
228
+ "'#{value}'"
229
+ when Time, DateTime
230
+ "'#{value.strftime('%Y-%m-%d %H:%M:%S')}'"
231
+ when Hash
232
+ value.inspect
233
+ else
234
+ value.inspect
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'tmpdir'
5
+ require 'generators/rider_kick/factory_generator'
6
+
7
+ RSpec.describe 'rider_kick:factory generator' do
8
+ let(:klass) { RiderKick::FactoryGenerator }
9
+
10
+ it 'generates factory with proper format' do
11
+ # Stub the model class
12
+ stub_const('Models', Module.new)
13
+ stub_const('Models::Article', Class.new do
14
+ def self.columns
15
+ [
16
+ Struct.new(:name, :type).new('id', :integer),
17
+ Struct.new(:name, :type).new('title', :string),
18
+ Struct.new(:name, :type).new('content', :text),
19
+ Struct.new(:name, :type).new('user_id', :integer),
20
+ Struct.new(:name, :type).new('published', :boolean),
21
+ Struct.new(:name, :type).new('created_at', :datetime),
22
+ Struct.new(:name, :type).new('updated_at', :datetime)
23
+ ]
24
+ end
25
+ end)
26
+
27
+ Dir.mktmpdir do |dir|
28
+ Dir.chdir(dir) do
29
+ FileUtils.mkdir_p('spec/factories/core')
30
+
31
+ instance = klass.new(['Models::Article', { 'scope' => 'core' }])
32
+ instance.generate_factory
33
+
34
+ factory_file = File.join('spec/factories/core/article.rb')
35
+ expect(File.exist?(factory_file)).to be true
36
+
37
+ content = File.read(factory_file)
38
+ expect(content).to include('factory :article')
39
+ expect(content).to include("class: 'Models::Article'")
40
+ expect(content).to include('title')
41
+ expect(content).to include('content')
42
+ expect(content).to include('published')
43
+ expect(content).not_to include('user_id')
44
+ expect(content).not_to include('created_at')
45
+ expect(content).not_to include('updated_at')
46
+ end
47
+ end
48
+ end
49
+
50
+ it 'skips all foreign key columns ending with _id' do
51
+ stub_const('Models', Module.new)
52
+ stub_const('Models::Comment', Class.new do
53
+ def self.columns
54
+ [
55
+ Struct.new(:name, :type).new('id', :integer),
56
+ Struct.new(:name, :type).new('body', :text),
57
+ Struct.new(:name, :type).new('user_id', :integer),
58
+ Struct.new(:name, :type).new('post_id', :integer),
59
+ Struct.new(:name, :type).new('author_id', :integer),
60
+ Struct.new(:name, :type).new('created_at', :datetime),
61
+ Struct.new(:name, :type).new('updated_at', :datetime)
62
+ ]
63
+ end
64
+ end)
65
+
66
+ Dir.mktmpdir do |dir|
67
+ Dir.chdir(dir) do
68
+ FileUtils.mkdir_p('spec/factories')
69
+
70
+ instance = klass.new(['Models::Comment'])
71
+ instance.generate_factory
72
+
73
+ factory_file = File.join('spec/factories/comment.rb')
74
+ content = File.read(factory_file)
75
+
76
+ expect(content).to include('body')
77
+ expect(content).not_to include('user_id')
78
+ expect(content).not_to include('post_id')
79
+ expect(content).not_to include('author_id')
80
+ end
81
+ end
82
+ end
83
+
84
+ it 'raises error when model not found' do
85
+ Dir.mktmpdir do |dir|
86
+ Dir.chdir(dir) do
87
+ instance = klass.new(['Models::NonExistent'])
88
+ expect { instance.generate_factory }
89
+ .to raise_error(RiderKick::ModelNotFoundError)
90
+ end
91
+ end
92
+ end
93
+
94
+ it 'generates factory with static values when --static option is used' do
95
+ stub_const('Models', Module.new)
96
+ stub_const('Models::User', Class.new do
97
+ def self.columns
98
+ [
99
+ Struct.new(:name, :type).new('id', :integer),
100
+ Struct.new(:name, :type).new('full_name', :string),
101
+ Struct.new(:name, :type).new('email', :string),
102
+ Struct.new(:name, :type).new('age', :integer),
103
+ Struct.new(:name, :type).new('active', :boolean),
104
+ Struct.new(:name, :type).new('created_at', :datetime),
105
+ Struct.new(:name, :type).new('updated_at', :datetime)
106
+ ]
107
+ end
108
+ end)
109
+
110
+ Dir.mktmpdir do |dir|
111
+ Dir.chdir(dir) do
112
+ FileUtils.mkdir_p('spec/factories')
113
+
114
+ instance = klass.new(['Models::User'], {}, {})
115
+ instance.options = { static: true }
116
+ instance.generate_factory
117
+
118
+ factory_file = File.join('spec/factories/user.rb')
119
+ content = File.read(factory_file)
120
+
121
+ expect(content).to include('full_name')
122
+ expect(content).to include('email')
123
+ expect(content).to include('age')
124
+ expect(content).to include('active')
125
+ # Static values should be quoted strings or literal values, not Faker calls
126
+ expect(content).not_to include('Faker::')
127
+ # Should contain actual static values
128
+ expect(content).to match(/full_name \{ '[^']+' \}/)
129
+ expect(content).to match(/email \{ '[^']+@[^']+' \}/)
130
+ end
131
+ end
132
+ end
133
+
134
+ it 'keeps Time.zone.now for time fields even with --static option' do
135
+ stub_const('Models', Module.new)
136
+ stub_const('Models::Event', Class.new do
137
+ def self.columns
138
+ [
139
+ Struct.new(:name, :type).new('id', :integer),
140
+ Struct.new(:name, :type).new('title', :string),
141
+ Struct.new(:name, :type).new('start_time', :datetime),
142
+ Struct.new(:name, :type).new('end_time', :timestamp),
143
+ Struct.new(:name, :type).new('reminder_time', :time),
144
+ Struct.new(:name, :type).new('created_at', :datetime),
145
+ Struct.new(:name, :type).new('updated_at', :datetime)
146
+ ]
147
+ end
148
+ end)
149
+
150
+ Dir.mktmpdir do |dir|
151
+ Dir.chdir(dir) do
152
+ FileUtils.mkdir_p('spec/factories')
153
+
154
+ instance = klass.new(['Models::Event'], {}, {})
155
+ instance.options = { static: true }
156
+ instance.generate_factory
157
+
158
+ factory_file = File.join('spec/factories/event.rb')
159
+ content = File.read(factory_file)
160
+
161
+ # Title should be static
162
+ expect(content).to match(/title \{ '[^']+' \}/)
163
+
164
+ # Time fields should NOT be static, should remain Time.zone.now
165
+ expect(content).to include('start_time { Time.zone.now }')
166
+ expect(content).to include('end_time { Time.zone.now }')
167
+ expect(content).to include('reminder_time { Time.zone.now }')
168
+
169
+ # Should not contain static datetime strings
170
+ expect(content).not_to match(/start_time \{ '[0-9]{4}-[0-9]{2}-[0-9]{2}/)
171
+ expect(content).not_to match(/end_time \{ '[0-9]{4}-[0-9]{2}-[0-9]{2}/)
172
+ end
173
+ end
174
+ end
175
+ end
@@ -4,22 +4,21 @@
4
4
  require 'rails/generators'
5
5
  require 'tmpdir'
6
6
  require 'fileutils'
7
- require 'ostruct'
8
7
  require 'generators/rider_kick/scaffold_generator'
9
8
 
10
9
  RSpec.describe 'repositories scaffolded content' do
11
10
  let(:klass) { RiderKick::ScaffoldGenerator }
12
11
 
13
- it 'memuat filter resource_owner + pagination + (opsional) search_able' do
12
+ it 'memuat filter resource_owner + pagination + (opsional) search_able ketika resource_owner_id ada di contract' do
14
13
  Dir.mktmpdir do |dir|
15
14
  Dir.chdir(dir) do
16
- FileUtils.mkdir_p %w[
17
- app/domains/core/use_cases
18
- app/domains/core/repositories
19
- app/domains/core/builders
20
- app/domains/core/entities
21
- app/models/models
22
- db/structures
15
+ FileUtils.mkdir_p [
16
+ RiderKick.configuration.domains_path + '/core/use_cases',
17
+ RiderKick.configuration.domains_path + '/core/repositories',
18
+ RiderKick.configuration.domains_path + '/core/builders',
19
+ RiderKick.configuration.domains_path + '/core/entities',
20
+ 'app/models/models',
21
+ 'db/structures'
23
22
  ]
24
23
  File.write('app/models/models/user.rb', "class Models::User < ApplicationRecord; end\n")
25
24
  File.write('db/structures/users_structure.yaml', <<~YAML)
@@ -27,35 +26,109 @@ RSpec.describe 'repositories scaffolded content' do
27
26
  resource_name: users
28
27
  actor: owner
29
28
  resource_owner_id: owner_id
29
+ resource_owner: owner
30
30
  uploaders: []
31
31
  search_able: [name]
32
32
  domains:
33
- action_list: { use_case: { contract: [] } }
34
- action_fetch_by_id: { use_case: { contract: [] } }
35
- action_create: { use_case: { contract: [] } }
36
- action_update: { use_case: { contract: [] } }
37
- action_destroy: { use_case: { contract: [] } }
38
- entity: { skipped_fields: [id, created_at, updated_at] }
33
+ action_list:
34
+ use_case:
35
+ contract:
36
+ - "required(:owner_id).filled(:string)"
37
+ action_fetch_by_id:
38
+ use_case:
39
+ contract:
40
+ - "required(:owner_id).filled(:string)"
41
+ action_create:
42
+ use_case:
43
+ contract: []
44
+ action_update:
45
+ use_case:
46
+ contract:
47
+ - "required(:owner_id).filled(:string)"
48
+ action_destroy:
49
+ use_case:
50
+ contract:
51
+ - "required(:owner_id).filled(:string)"
52
+ entity: { db_attributes: [id, created_at, updated_at] }
39
53
  YAML
40
54
 
41
55
  klass.new(['users']).generate_use_case
42
56
 
43
- list_repo = File.read('app/domains/core/repositories/users/list_user.rb')
44
- expect(list_repo).to match(/resource_owner_id|owner_id|account_id/i) # salah satu pola filter kepemilikan
57
+ list_repo = File.read(RiderKick.configuration.domains_path + '/repositories/users/list_user.rb')
58
+ expect(list_repo).to match(/\.where\(owner_id: @params\.owner_id\)/) # filter resource_owner_id digunakan
45
59
  expect(list_repo).to match(/paginate|per_page|page/i) # pagination hook
46
60
  expect(list_repo).to match(/name|search/i) # search_able minimal
47
61
 
48
- fetch_repo = File.read('app/domains/core/repositories/users/fetch_user_by_id.rb')
49
- expect(fetch_repo).to match(/find_by|where\(.+id:/i)
62
+ fetch_repo = File.read(RiderKick.configuration.domains_path + '/repositories/users/fetch_user_by_id.rb')
63
+ expect(fetch_repo).to match(/find_by\(id: @id, owner_id: @params\.owner_id\)/)
50
64
 
51
- create_repo = File.read('app/domains/core/repositories/users/create_user.rb')
52
- update_repo = File.read('app/domains/core/repositories/users/update_user.rb')
53
- destroy_repo = File.read('app/domains/core/repositories/users/destroy_user.rb')
65
+ create_repo = File.read(RiderKick.configuration.domains_path + '/repositories/users/create_user.rb')
66
+ update_repo = File.read(RiderKick.configuration.domains_path + '/repositories/users/update_user.rb')
67
+ destroy_repo = File.read(RiderKick.configuration.domains_path + '/repositories/users/destroy_user.rb')
54
68
 
55
69
  # repos utama terbentuk & memanggil ActiveRecord target
56
70
  [create_repo, update_repo, destroy_repo].each do |src|
57
71
  expect(src).to include('Models::User')
58
72
  end
73
+
74
+ # Update dan destroy harus punya filter karena ada di contract
75
+ expect(update_repo).to match(/find_by\(id: @id, owner_id: @params\.owner_id\)/)
76
+ expect(destroy_repo).to match(/find_by\(id: @id, owner_id: @params\.owner_id\)/)
77
+ end
78
+ end
79
+ end
80
+
81
+ it 'tidak memuat filter resource_owner ketika resource_owner_id tidak ada di contract' do
82
+ Dir.mktmpdir do |dir|
83
+ Dir.chdir(dir) do
84
+ FileUtils.mkdir_p [
85
+ RiderKick.configuration.domains_path + '/core/use_cases',
86
+ RiderKick.configuration.domains_path + '/core/repositories',
87
+ RiderKick.configuration.domains_path + '/core/builders',
88
+ RiderKick.configuration.domains_path + '/core/entities',
89
+ 'app/models/models',
90
+ 'db/structures'
91
+ ]
92
+ File.write('app/models/models/user.rb', "class Models::User < ApplicationRecord; end\n")
93
+ File.write('db/structures/users_structure.yaml', <<~YAML)
94
+ model: Models::User
95
+ resource_name: users
96
+ actor: owner
97
+ resource_owner_id: owner_id
98
+ resource_owner: owner
99
+ uploaders: []
100
+ search_able: [name]
101
+ domains:
102
+ action_list:
103
+ use_case:
104
+ contract: []
105
+ # owner_id TIDAK ADA di contract
106
+ action_fetch_by_id:
107
+ use_case:
108
+ contract: []
109
+ action_create:
110
+ use_case:
111
+ contract: []
112
+ action_update:
113
+ use_case:
114
+ contract: []
115
+ action_destroy:
116
+ use_case:
117
+ contract: []
118
+ entity: { db_attributes: [id, created_at, updated_at] }
119
+ YAML
120
+
121
+ klass.new(['users']).generate_use_case
122
+
123
+ list_repo = File.read(RiderKick.configuration.domains_path + '/repositories/users/list_user.rb')
124
+ expect(list_repo).not_to match(/\.where\(owner_id:/) # filter resource_owner_id TIDAK digunakan
125
+ expect(list_repo).to match(/resources = Models::User\s*$/) # langsung query tanpa filter
126
+ expect(list_repo).to match(/paginate|per_page|page/i) # pagination hook tetap ada
127
+ expect(list_repo).to match(/name|search/i) # search_able minimal
128
+
129
+ fetch_repo = File.read(RiderKick.configuration.domains_path + '/repositories/users/fetch_user_by_id.rb')
130
+ expect(fetch_repo).to match(/find_by\(id: @id\)/) # tanpa filter owner_id
131
+ expect(fetch_repo).not_to match(/find_by\(id: @id, owner_id:/)
59
132
  end
60
133
  end
61
134
  end