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.
- checksums.yaml +4 -4
- data/README.md +629 -25
- data/lib/generators/rider_kick/USAGE +2 -0
- data/lib/generators/rider_kick/base_generator.rb +190 -0
- data/lib/generators/rider_kick/clean_arch_generator.rb +235 -45
- data/lib/generators/rider_kick/clean_arch_generator_engine_spec.rb +359 -0
- data/lib/generators/rider_kick/clean_arch_generator_factory_bot_spec.rb +131 -0
- data/lib/generators/rider_kick/entity_type_mapping_spec.rb +22 -13
- data/lib/generators/rider_kick/errors.rb +42 -0
- data/lib/generators/rider_kick/factory_generator.rb +238 -0
- data/lib/generators/rider_kick/factory_generator_spec.rb +175 -0
- data/lib/generators/rider_kick/repositories_contract_spec.rb +95 -22
- data/lib/generators/rider_kick/scaffold_generator.rb +377 -62
- data/lib/generators/rider_kick/scaffold_generator_builder_uploaders_spec.rb +119 -14
- data/lib/generators/rider_kick/scaffold_generator_conditional_filtering_spec.rb +820 -0
- data/lib/generators/rider_kick/scaffold_generator_contracts_spec.rb +37 -10
- data/lib/generators/rider_kick/scaffold_generator_contracts_with_scope_spec.rb +40 -11
- data/lib/generators/rider_kick/scaffold_generator_engine_spec.rb +221 -0
- data/lib/generators/rider_kick/scaffold_generator_idempotent_spec.rb +38 -13
- data/lib/generators/rider_kick/scaffold_generator_list_spec_format_spec.rb +153 -0
- data/lib/generators/rider_kick/scaffold_generator_rspec_spec.rb +347 -0
- data/lib/generators/rider_kick/scaffold_generator_success_spec.rb +31 -12
- data/lib/generators/rider_kick/scaffold_generator_with_scope_spec.rb +32 -11
- data/lib/generators/rider_kick/structure_generator.rb +154 -43
- data/lib/generators/rider_kick/structure_generator_comprehensive_spec.rb +598 -0
- data/lib/generators/rider_kick/structure_generator_engine_spec.rb +279 -0
- data/lib/generators/rider_kick/structure_generator_spec.rb +3 -3
- data/lib/generators/rider_kick/structure_generator_success_spec.rb +33 -5
- data/lib/generators/rider_kick/structure_generator_unit_spec.rb +2202 -0
- data/lib/generators/rider_kick/templates/.rubocop.yml +5 -4
- data/lib/generators/rider_kick/templates/config/initializers/version.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/db/migrate/20220613145533_init_database.rb +1 -1
- data/lib/generators/rider_kick/templates/db/structures/example.yaml.tt +140 -66
- data/lib/generators/rider_kick/templates/domains/core/builders/builder.rb.tt +36 -10
- data/lib/generators/rider_kick/templates/domains/core/builders/builder_spec.rb.tt +219 -0
- data/lib/generators/rider_kick/templates/domains/core/builders/error.rb.tt +2 -2
- data/lib/generators/rider_kick/templates/domains/core/builders/pagination.rb.tt +2 -2
- data/lib/generators/rider_kick/templates/domains/core/entities/entity.rb.tt +32 -14
- data/lib/generators/rider_kick/templates/domains/core/entities/error.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/domains/core/entities/pagination.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/domains/core/repositories/abstract_repository.rb.tt +4 -4
- data/lib/generators/rider_kick/templates/domains/core/repositories/create.rb.tt +2 -2
- data/lib/generators/rider_kick/templates/domains/core/repositories/create_spec.rb.tt +78 -0
- data/lib/generators/rider_kick/templates/domains/core/repositories/destroy.rb.tt +2 -2
- data/lib/generators/rider_kick/templates/domains/core/repositories/destroy_spec.rb.tt +88 -0
- data/lib/generators/rider_kick/templates/domains/core/repositories/fetch_by_id.rb.tt +3 -3
- data/lib/generators/rider_kick/templates/domains/core/repositories/fetch_by_id_spec.rb.tt +62 -0
- data/lib/generators/rider_kick/templates/domains/core/repositories/list.rb.tt +13 -8
- data/lib/generators/rider_kick/templates/domains/core/repositories/list_spec.rb.tt +190 -0
- data/lib/generators/rider_kick/templates/domains/core/repositories/update.rb.tt +4 -4
- data/lib/generators/rider_kick/templates/domains/core/repositories/update_spec.rb.tt +119 -0
- data/lib/generators/rider_kick/templates/domains/core/use_cases/contract/default.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/domains/core/use_cases/contract/pagination.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/domains/core/use_cases/create.rb.tt +3 -7
- data/lib/generators/rider_kick/templates/domains/core/use_cases/create_spec.rb.tt +71 -0
- data/lib/generators/rider_kick/templates/domains/core/use_cases/destroy.rb.tt +3 -7
- data/lib/generators/rider_kick/templates/domains/core/use_cases/destroy_spec.rb.tt +62 -0
- data/lib/generators/rider_kick/templates/domains/core/use_cases/fetch_by_id.rb.tt +3 -7
- data/lib/generators/rider_kick/templates/domains/core/use_cases/fetch_by_id_spec.rb.tt +62 -0
- data/lib/generators/rider_kick/templates/domains/core/use_cases/get_version.rb.tt +2 -2
- data/lib/generators/rider_kick/templates/domains/core/use_cases/list.rb.tt +3 -7
- data/lib/generators/rider_kick/templates/domains/core/use_cases/list_spec.rb.tt +64 -0
- data/lib/generators/rider_kick/templates/domains/core/use_cases/update.rb.tt +3 -7
- data/lib/generators/rider_kick/templates/domains/core/use_cases/update_spec.rb.tt +73 -0
- data/lib/generators/rider_kick/templates/domains/core/utils/abstract_utils.rb.tt +3 -3
- data/lib/generators/rider_kick/templates/domains/core/utils/request_methods.rb.tt +1 -1
- data/lib/generators/rider_kick/templates/env.development +1 -1
- data/lib/generators/rider_kick/templates/env.production +1 -1
- data/lib/generators/rider_kick/templates/env.test +1 -1
- data/lib/generators/rider_kick/templates/models/{application_record.rb → application_record.rb.tt} +3 -1
- data/lib/generators/rider_kick/templates/models/model_spec.rb.tt +68 -0
- data/lib/generators/rider_kick/templates/spec/factories/.gitkeep +19 -0
- data/lib/generators/rider_kick/templates/spec/factories/factory.rb.tt +8 -0
- data/lib/generators/rider_kick/templates/spec/rails_helper.rb +2 -0
- data/lib/generators/rider_kick/templates/spec/support/class_stubber.rb +148 -0
- data/lib/generators/rider_kick/templates/spec/support/factory_bot.rb +34 -0
- data/lib/generators/rider_kick/templates/spec/support/faker.rb +61 -0
- data/lib/rider-kick.rb +8 -6
- data/lib/rider_kick/builders/abstract_active_record_entity_builder_spec.rb +644 -0
- data/lib/rider_kick/configuration.rb +238 -0
- data/lib/rider_kick/configuration_engine_spec.rb +377 -0
- data/lib/rider_kick/entities/failure_details.rb +1 -1
- data/lib/rider_kick/entities/failure_details_spec.rb +1 -1
- data/lib/rider_kick/matchers/use_case_result.rb +1 -1
- data/lib/rider_kick/use_cases/abstract_use_case.rb +1 -1
- data/lib/rider_kick/version.rb +1 -1
- metadata +129 -8
- 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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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('
|
|
44
|
-
expect(list_repo).to match(
|
|
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('
|
|
49
|
-
expect(fetch_repo).to match(/find_by
|
|
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('
|
|
52
|
-
update_repo = File.read('
|
|
53
|
-
destroy_repo = File.read('
|
|
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
|