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
|
@@ -1,14 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'active_support/inflector'
|
|
5
|
+
require 'active_support/core_ext/object/blank'
|
|
6
|
+
require 'active_support/core_ext/enumerable'
|
|
7
|
+
require 'hashie'
|
|
1
8
|
require 'yaml'
|
|
9
|
+
require_relative 'base_generator'
|
|
10
|
+
require_relative '../../rider-kick'
|
|
11
|
+
|
|
2
12
|
module RiderKick
|
|
3
|
-
class ScaffoldGenerator <
|
|
13
|
+
class ScaffoldGenerator < BaseGenerator
|
|
4
14
|
source_root File.expand_path('templates', __dir__)
|
|
5
15
|
|
|
6
16
|
argument :arg_structure, type: :string, default: '', banner: ''
|
|
7
|
-
argument :arg_scope, type: :hash, default:
|
|
17
|
+
argument :arg_scope, type: :hash, default: {}, banner: 'scope:dashboard'
|
|
18
|
+
class_option :engine, type: :string, default: nil, desc: 'Specify engine name (e.g., Core, Admin)'
|
|
19
|
+
class_option :domain, type: :string, default: '', desc: 'Specify domain scope (e.g., core/, admin/, api/v1/)'
|
|
8
20
|
|
|
9
21
|
def generate_use_case
|
|
22
|
+
configure_engine
|
|
10
23
|
validation!
|
|
11
24
|
setup_variables
|
|
25
|
+
validate_repository_filters! # ← validate filter fields exist (after setup_variables)
|
|
26
|
+
validate_entity_fields! # ← validate entity db_attributes exist (after setup_variables)
|
|
12
27
|
|
|
13
28
|
generate_files('create')
|
|
14
29
|
generate_files('update')
|
|
@@ -18,72 +33,267 @@ module RiderKick
|
|
|
18
33
|
|
|
19
34
|
set_uploader_in_model
|
|
20
35
|
copy_builder_and_entity_files
|
|
36
|
+
generate_spec_files
|
|
21
37
|
end
|
|
22
38
|
|
|
23
39
|
private
|
|
24
40
|
|
|
41
|
+
def domain_class_name
|
|
42
|
+
# Convert domain scope to class name
|
|
43
|
+
# Engine: "<Engine>" for ApplicationRecord, "<Engine>::<Domain>" for other classes
|
|
44
|
+
# Main app: "" for ApplicationRecord, "<AppName>" for root domain, "<Domain>" for scoped domain
|
|
45
|
+
scope = RiderKick.configuration.domain_scope.chomp('/')
|
|
46
|
+
|
|
47
|
+
if RiderKick.configuration.engine_name.present?
|
|
48
|
+
# Engine context: domain_scope always starts with engine name
|
|
49
|
+
engine_prefix = RiderKick.configuration.engine_name.camelize
|
|
50
|
+
engine_underscored = RiderKick.configuration.engine_name.underscore
|
|
51
|
+
|
|
52
|
+
if scope == engine_underscored
|
|
53
|
+
# Default engine domain: engines/my_engine/app/domains/my_engine/
|
|
54
|
+
engine_prefix
|
|
55
|
+
else
|
|
56
|
+
# Engine with sub-domain: engines/my_engine/app/domains/my_engine/admin/
|
|
57
|
+
# Remove engine prefix from scope and create namespace
|
|
58
|
+
sub_scope = scope.sub(/^#{engine_underscored}\//, '')
|
|
59
|
+
if sub_scope.empty?
|
|
60
|
+
engine_prefix
|
|
61
|
+
else
|
|
62
|
+
"#{engine_prefix}::#{sub_scope.split('/').map(&:camelize).join('::')}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
elsif scope.empty?
|
|
66
|
+
# Main app context
|
|
67
|
+
# Root domain in main app: use application name
|
|
68
|
+
begin
|
|
69
|
+
Rails.application&.class&.module_parent_name || 'MyApp'
|
|
70
|
+
rescue
|
|
71
|
+
'MyApp' # Fallback for test environment
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
# Scoped domain in main app: use domain name only
|
|
75
|
+
scope.split('/').map(&:camelize).join('::')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
25
79
|
def validation!
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
80
|
+
validate_domains_path!
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_repository_filters!
|
|
84
|
+
return if @repository_list_filters.empty?
|
|
85
|
+
|
|
86
|
+
@repository_list_filters.each do |filter_hash|
|
|
87
|
+
# Filter hash format: "{ field: 'name', type: 'search' }"
|
|
88
|
+
# Kita perlu extract field name dari string ini
|
|
89
|
+
match = filter_hash.match(/field: '([^']+)'/)
|
|
90
|
+
next unless match
|
|
91
|
+
|
|
92
|
+
field_name = match[1]
|
|
93
|
+
|
|
94
|
+
unless @model_class.column_names.include?(field_name)
|
|
95
|
+
raise ValidationError.new(
|
|
96
|
+
"Repository filter error: Field '#{field_name}' tidak ditemukan di model #{@model_class}",
|
|
97
|
+
structure_file: "#{arg_structure}_structure.yaml",
|
|
98
|
+
field_name: field_name,
|
|
99
|
+
model_class: @model_class.to_s,
|
|
100
|
+
available_columns: @model_class.column_names
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def validate_entity_fields!
|
|
107
|
+
return if @entity_db_fields.empty?
|
|
108
|
+
|
|
109
|
+
missing_fields = @entity_db_fields - @model_class.column_names
|
|
110
|
+
|
|
111
|
+
if missing_fields.any?
|
|
112
|
+
raise ValidationError.new(
|
|
113
|
+
"Entity configuration error: Field(s) tidak ditemukan di model #{@model_class}",
|
|
114
|
+
structure_file: "#{arg_structure}_structure.yaml",
|
|
115
|
+
missing_fields: missing_fields,
|
|
116
|
+
model_class: @model_class.to_s,
|
|
117
|
+
available_columns: @model_class.column_names,
|
|
118
|
+
suggestion: "Update section 'entity.db_attributes' di YAML file"
|
|
119
|
+
)
|
|
29
120
|
end
|
|
30
121
|
end
|
|
31
122
|
|
|
32
123
|
def setup_variables
|
|
33
|
-
|
|
124
|
+
setup_structure_config
|
|
125
|
+
setup_model_variables
|
|
126
|
+
setup_contract_variables
|
|
127
|
+
setup_entity_variables
|
|
128
|
+
setup_repository_variables
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def setup_structure_config
|
|
132
|
+
# Determine structure file path based on engine configuration
|
|
133
|
+
structure_path = if RiderKick.configuration.engine_name.present?
|
|
134
|
+
# For engines, read structure file from engine's db/structures directory
|
|
135
|
+
engine_name = RiderKick.configuration.engine_name.downcase
|
|
136
|
+
"engines/#{engine_name}/db/structures/#{arg_structure}_structure.yaml"
|
|
137
|
+
else
|
|
138
|
+
# For main app, read from host's db/structures directory
|
|
139
|
+
"db/structures/#{arg_structure}_structure.yaml"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
validate_yaml_format!(structure_path)
|
|
143
|
+
config = YAML.load_file(structure_path)
|
|
34
144
|
@structure = Hashie::Mash.new(config)
|
|
145
|
+
end
|
|
35
146
|
|
|
36
|
-
|
|
37
|
-
model_name
|
|
147
|
+
def setup_model_variables
|
|
148
|
+
model_name = @structure.model
|
|
149
|
+
validate_model_exists!(model_name.camelize)
|
|
150
|
+
@model_class = model_name.camelize.constantize
|
|
151
|
+
|
|
152
|
+
@variable_subject = model_name.split('::').last.underscore.downcase
|
|
153
|
+
@subject_class = @variable_subject.camelize
|
|
154
|
+
@fields = contract_fields
|
|
155
|
+
|
|
156
|
+
# Column metadata
|
|
157
|
+
@columns_meta = columns_meta
|
|
158
|
+
@columns_meta_hash = @columns_meta.index_by { |c| c[:name] }
|
|
159
|
+
|
|
160
|
+
# Type mappings
|
|
161
|
+
@type_mapping = RiderKick.configuration.type_mapping
|
|
162
|
+
@entity_type_mapping = RiderKick.configuration.entity_type_mapping
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def setup_contract_variables
|
|
166
|
+
@services = @structure.domains || {}
|
|
167
|
+
|
|
168
|
+
# Membaca kontrak dinamis (dari Peningkatan #1)
|
|
169
|
+
# Pastikan selalu return array, bahkan jika nil
|
|
170
|
+
@contract_list = Array(@services.action_list&.use_case&.contract).compact
|
|
171
|
+
@contract_fetch_by_id = Array(@services.action_fetch_by_id&.use_case&.contract).compact
|
|
172
|
+
@contract_create = Array(@services.action_create&.use_case&.contract).compact
|
|
173
|
+
@contract_update = Array(@services.action_update&.use_case&.contract).compact
|
|
174
|
+
@contract_destroy = Array(@services.action_destroy&.use_case&.contract).compact
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def setup_entity_variables
|
|
38
178
|
resource_name = @structure.resource_name.singularize.underscore.downcase
|
|
39
179
|
entity = @structure.entity || {}
|
|
40
180
|
|
|
41
|
-
@actor = @structure.actor
|
|
42
|
-
@resource_owner_id = @structure.resource_owner_id
|
|
43
|
-
@uploaders = @structure.uploaders || []
|
|
44
|
-
@search_able = @structure.search_able || []
|
|
45
|
-
@services = @structure.domains || {}
|
|
46
|
-
@contract_list = @services.action_list.use_case.contract || []
|
|
47
|
-
@contract_fetch_by_id = @services.action_fetch_by_id.use_case.contract || []
|
|
48
|
-
@contract_create = @services.action_create.use_case.contract || []
|
|
49
|
-
@contract_update = @services.action_update.use_case.contract || []
|
|
50
|
-
@contract_destroy = @services.action_destroy.use_case.contract || []
|
|
51
|
-
@skipped_fields = entity.skipped_fields || []
|
|
52
|
-
@custom_fields = entity.custom_fields || []
|
|
53
|
-
|
|
54
|
-
@variable_subject = model_name.split('::').last.underscore.downcase
|
|
55
181
|
@scope_path = resource_name.pluralize.underscore.downcase
|
|
56
182
|
@scope_class = @scope_path.camelize
|
|
57
183
|
@scope_subject = @scope_path.singularize
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
184
|
+
|
|
185
|
+
# Membaca definisi uploader baru (array of hashes)
|
|
186
|
+
@uploaders = (@structure.uploaders || []).map { |up| Hashie::Mash.new(up) }
|
|
187
|
+
|
|
188
|
+
# Membaca atribut DB eksplisit (array string)
|
|
189
|
+
@entity_db_fields = entity.respond_to?(:db_attributes) ? entity.db_attributes || [] : []
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def setup_repository_variables
|
|
193
|
+
@actor = @structure.actor
|
|
194
|
+
@resource_owner_id = @structure.resource_owner_id
|
|
195
|
+
@resource_owner = @structure.resource_owner
|
|
196
|
+
@search_able = @structure.search_able
|
|
197
|
+
|
|
198
|
+
# Membaca DSL filter repositori baru (Peningkatan #2)
|
|
199
|
+
@repository_list_filters = @services.action_list&.repository&.filters || []
|
|
200
|
+
|
|
201
|
+
# Route scope
|
|
202
|
+
@route_scope_path = arg_scope.fetch('scope', '').to_s.downcase
|
|
203
|
+
@route_scope_class = @route_scope_path.camelize
|
|
204
|
+
|
|
205
|
+
# Baca actor_id dari structure.yaml jika ada, jika tidak generate dari actor
|
|
206
|
+
@actor_id = if @structure.actor_id.present?
|
|
207
|
+
@structure.actor_id.to_s
|
|
208
|
+
elsif @actor.present?
|
|
209
|
+
"#{@actor.to_s.downcase}_id"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Set flag untuk setiap action apakah resource_owner_id atau actor_id ada di contract
|
|
213
|
+
# Ini digunakan di template repository untuk conditional logic
|
|
214
|
+
@has_resource_owner_id_in_list_contract = field_exists_in_contract?(@resource_owner_id, 'list')
|
|
215
|
+
@has_resource_owner_id_in_fetch_by_id_contract = field_exists_in_contract?(@resource_owner_id, 'fetch_by_id')
|
|
216
|
+
@has_resource_owner_id_in_create_contract = field_exists_in_contract?(@resource_owner_id, 'create')
|
|
217
|
+
@has_resource_owner_id_in_update_contract = field_exists_in_contract?(@resource_owner_id, 'update')
|
|
218
|
+
@has_resource_owner_id_in_destroy_contract = field_exists_in_contract?(@resource_owner_id, 'destroy')
|
|
219
|
+
|
|
220
|
+
@has_actor_id_in_list_contract = field_exists_in_contract?(@actor_id, 'list')
|
|
221
|
+
@has_actor_id_in_fetch_by_id_contract = field_exists_in_contract?(@actor_id, 'fetch_by_id')
|
|
222
|
+
@has_actor_id_in_create_contract = field_exists_in_contract?(@actor_id, 'create')
|
|
223
|
+
@has_actor_id_in_update_contract = field_exists_in_contract?(@actor_id, 'update')
|
|
224
|
+
@has_actor_id_in_destroy_contract = field_exists_in_contract?(@actor_id, 'destroy')
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Check apakah field (actor_id atau resource_owner_id) ada di contract untuk action tertentu
|
|
228
|
+
def field_exists_in_contract?(field_name, action)
|
|
229
|
+
return false if field_name.blank?
|
|
230
|
+
|
|
231
|
+
# Get contract untuk action tertentu
|
|
232
|
+
contract = case action.to_s
|
|
233
|
+
when 'list'
|
|
234
|
+
@contract_list
|
|
235
|
+
when 'fetch', 'fetch_by_id'
|
|
236
|
+
@contract_fetch_by_id
|
|
237
|
+
when 'create'
|
|
238
|
+
@contract_create
|
|
239
|
+
when 'update'
|
|
240
|
+
@contract_update
|
|
241
|
+
when 'destroy'
|
|
242
|
+
@contract_destroy
|
|
243
|
+
else
|
|
244
|
+
[]
|
|
245
|
+
end
|
|
246
|
+
contract ||= []
|
|
247
|
+
# Check apakah contract string mengandung field name
|
|
248
|
+
# Pattern: "required(:field_name)" atau "optional(:field_name)"
|
|
249
|
+
# Contoh: "required(:account_id).filled(:string)" -> match untuk "account_id"
|
|
250
|
+
# Pattern lebih robust: cari :field_name atau (:field_name) di dalam string
|
|
251
|
+
return false if contract.empty?
|
|
252
|
+
|
|
253
|
+
contract.any? do |contract_line|
|
|
254
|
+
contract_str = contract_line.to_s.strip
|
|
255
|
+
# Match pattern: required(:field_name) atau optional(:field_name) atau :field_name
|
|
256
|
+
# Pattern: cari :field_name atau (:field_name) di dalam string
|
|
257
|
+
# Escaped field_name untuk handle special characters
|
|
258
|
+
escaped_field = Regexp.escape(field_name.to_s)
|
|
259
|
+
contract_str.match?(/[:\(]#{escaped_field}[\):]/)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Helper method untuk template: check apakah resource_owner_id ada di contract untuk action tertentu
|
|
264
|
+
def has_resource_owner_id_in_contract?(action)
|
|
265
|
+
case action.to_s
|
|
266
|
+
when 'list'
|
|
267
|
+
@has_resource_owner_id_in_list_contract
|
|
268
|
+
when 'fetch', 'fetch_by_id'
|
|
269
|
+
@has_resource_owner_id_in_fetch_by_id_contract
|
|
270
|
+
when 'create'
|
|
271
|
+
@has_resource_owner_id_in_create_contract
|
|
272
|
+
when 'update'
|
|
273
|
+
@has_resource_owner_id_in_update_contract
|
|
274
|
+
when 'destroy'
|
|
275
|
+
@has_resource_owner_id_in_destroy_contract
|
|
276
|
+
else
|
|
277
|
+
false
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Helper method untuk template: check apakah actor_id ada di contract untuk action tertentu
|
|
282
|
+
def has_actor_id_in_contract?(action)
|
|
283
|
+
case action.to_s
|
|
284
|
+
when 'list'
|
|
285
|
+
@has_actor_id_in_list_contract
|
|
286
|
+
when 'fetch', 'fetch_by_id'
|
|
287
|
+
@has_actor_id_in_fetch_by_id_contract
|
|
288
|
+
when 'create'
|
|
289
|
+
@has_actor_id_in_create_contract
|
|
290
|
+
when 'update'
|
|
291
|
+
@has_actor_id_in_update_contract
|
|
292
|
+
when 'destroy'
|
|
293
|
+
@has_actor_id_in_destroy_contract
|
|
294
|
+
else
|
|
295
|
+
false
|
|
296
|
+
end
|
|
87
297
|
end
|
|
88
298
|
|
|
89
299
|
def is_singular?(str)
|
|
@@ -92,11 +302,49 @@ module RiderKick
|
|
|
92
302
|
|
|
93
303
|
def set_uploader_in_model
|
|
94
304
|
@uploaders.each do |uploader|
|
|
95
|
-
method_strategy = 'has_many_attached'
|
|
96
|
-
|
|
97
|
-
|
|
305
|
+
method_strategy = uploader.type == 'single' ? 'has_one_attached' : 'has_many_attached'
|
|
306
|
+
uploader_name = uploader.name
|
|
307
|
+
model_path = model_file_path(@model_class, @variable_subject)
|
|
308
|
+
|
|
309
|
+
unless File.exist?(model_path)
|
|
310
|
+
say "Skip attaching #{uploader_name}: model file not found: #{model_path}", :yellow
|
|
311
|
+
next
|
|
98
312
|
end
|
|
99
|
-
|
|
313
|
+
|
|
314
|
+
content = File.read(model_path)
|
|
315
|
+
|
|
316
|
+
# More robust check: look for has_one_attached/has_many_attached with the uploader name
|
|
317
|
+
# Pattern matches: has_one_attached :name or has_one_attached :name, dependent: ...
|
|
318
|
+
attachment_pattern = /#{Regexp.escape(method_strategy)}\s+:#{Regexp.escape(uploader_name)}\b/
|
|
319
|
+
|
|
320
|
+
if content.match?(attachment_pattern)
|
|
321
|
+
say "Skip attaching #{uploader_name}: already present in #{model_path}", :blue
|
|
322
|
+
next
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
line_to_insert = " #{method_strategy} :#{uploader_name}, dependent: :purge\n"
|
|
326
|
+
class_anchor_regex = /class #{Regexp.escape(@model_class.to_s)} < ApplicationRecord[^\n]*\n/
|
|
327
|
+
|
|
328
|
+
inject_into_file model_path, line_to_insert, after: class_anchor_regex
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def model_file_path(model_class, variable_subject)
|
|
333
|
+
# Extract namespace dari model class
|
|
334
|
+
# Models::User -> namespace setelah Models adalah []
|
|
335
|
+
# Models::EngineName::User -> namespace setelah Models adalah [EngineName]
|
|
336
|
+
full_namespace = model_class.to_s.deconstantize
|
|
337
|
+
namespace_parts = full_namespace.split('::').reject(&:empty?)
|
|
338
|
+
|
|
339
|
+
# Jika model_class mengandung Models::EngineName::User, maka path ke engine
|
|
340
|
+
# Jika model_class Models::User, maka path ke main app
|
|
341
|
+
if namespace_parts.length > 1 && namespace_parts.first == 'Models'
|
|
342
|
+
# Engine: Models::EngineName::User -> app/models/<engine_name>/<model>.rb
|
|
343
|
+
engine_name_part = namespace_parts[1].underscore
|
|
344
|
+
File.join('app/models', engine_name_part, "#{variable_subject}.rb")
|
|
345
|
+
else
|
|
346
|
+
# Main app: Models::User -> app/models/models/<model>.rb
|
|
347
|
+
File.join(RiderKick.configuration.models_path, "#{variable_subject}.rb")
|
|
100
348
|
end
|
|
101
349
|
end
|
|
102
350
|
|
|
@@ -107,22 +355,50 @@ module RiderKick
|
|
|
107
355
|
@use_case_class = use_case_filename.camelize
|
|
108
356
|
@repository_class = repository_filename.camelize
|
|
109
357
|
|
|
110
|
-
|
|
111
|
-
template "domains/core/
|
|
358
|
+
# Generate code files
|
|
359
|
+
template "domains/core/use_cases/#{action + suffix}.rb.tt", File.join(RiderKick.configuration.domains_path, 'use_cases', @route_scope_path.to_s, @scope_path.to_s, "#{use_case_filename}.rb")
|
|
360
|
+
template "domains/core/repositories/#{action + suffix}.rb.tt", File.join(RiderKick.configuration.domains_path, 'repositories', @scope_path.to_s, "#{repository_filename}.rb")
|
|
361
|
+
|
|
362
|
+
# Generate spec files
|
|
363
|
+
generate_use_case_spec(action, suffix, use_case_filename)
|
|
364
|
+
generate_repository_spec(action, suffix, repository_filename)
|
|
112
365
|
end
|
|
113
366
|
|
|
114
367
|
def copy_builder_and_entity_files
|
|
115
|
-
template 'domains/core/builders/builder.rb.tt', File.join(
|
|
116
|
-
template 'domains/core/entities/entity.rb.tt', File.join(
|
|
368
|
+
template 'domains/core/builders/builder.rb.tt', File.join(RiderKick.configuration.domains_path, 'builders', "#{@variable_subject}.rb")
|
|
369
|
+
template 'domains/core/entities/entity.rb.tt', File.join(RiderKick.configuration.domains_path, 'entities', "#{@variable_subject}.rb")
|
|
117
370
|
end
|
|
118
371
|
|
|
119
372
|
def contract_fields
|
|
120
|
-
|
|
121
|
-
|
|
373
|
+
@model_class.columns.reject { |c| ['id', 'created_at', 'updated_at', 'type'].include?(c.name.to_s) }
|
|
374
|
+
.map { |c| c.name.to_s }
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# --- AWAL BLOK MODIFIKASI: (PERBAIKAN KEGAGALAN #1) ---
|
|
378
|
+
# Menambahkan helper-helper ini dari structure_generator
|
|
379
|
+
def columns_meta
|
|
380
|
+
@model_class.columns.map do |c|
|
|
381
|
+
{
|
|
382
|
+
name: c.name.to_s,
|
|
383
|
+
type: c.type,
|
|
384
|
+
sql_type: (c.respond_to?(:sql_type) ? c.sql_type : nil),
|
|
385
|
+
null: (c.respond_to?(:null) ? c.null : nil),
|
|
386
|
+
default: (c.respond_to?(:default) ? c.default : nil),
|
|
387
|
+
precision: (c.respond_to?(:precision) ? c.precision : nil),
|
|
388
|
+
scale: (c.respond_to?(:scale) ? c.scale : nil),
|
|
389
|
+
limit: (c.respond_to?(:limit) ? c.limit : nil)
|
|
390
|
+
}
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def get_column_meta(field)
|
|
395
|
+
@columns_meta_hash[field.to_s] || {}
|
|
122
396
|
end
|
|
397
|
+
# --- AKHIR BLOK MODIFIKASI ---
|
|
123
398
|
|
|
124
399
|
def get_column_type(field)
|
|
125
|
-
@uploaders.
|
|
400
|
+
is_uploader = @uploaders.any? { |up| up.name == field.to_s }
|
|
401
|
+
is_uploader ? 'upload' : @model_class.columns_hash[field.to_s].type
|
|
126
402
|
end
|
|
127
403
|
|
|
128
404
|
def root_path_app
|
|
@@ -136,5 +412,44 @@ module RiderKick
|
|
|
136
412
|
def build_repository_filename(action, suffix = '')
|
|
137
413
|
"#{action}_#{@variable_subject}#{suffix}"
|
|
138
414
|
end
|
|
415
|
+
|
|
416
|
+
def generate_use_case_spec(action, suffix, use_case_filename)
|
|
417
|
+
template_name = "domains/core/use_cases/#{action + suffix}_spec.rb.tt"
|
|
418
|
+
spec_path = File.join(RiderKick.configuration.domains_path, 'use_cases', @route_scope_path.to_s, @scope_path.to_s, "#{use_case_filename}_spec.rb")
|
|
419
|
+
|
|
420
|
+
if File.exist?(File.join(self.class.source_root, template_name))
|
|
421
|
+
template template_name, spec_path
|
|
422
|
+
else
|
|
423
|
+
say "Warning: Spec template not found: #{template_name}", :yellow
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def generate_repository_spec(action, suffix, repository_filename)
|
|
428
|
+
template_name = "domains/core/repositories/#{action + suffix}_spec.rb.tt"
|
|
429
|
+
spec_path = File.join(RiderKick.configuration.domains_path, 'repositories', @scope_path.to_s, "#{repository_filename}_spec.rb")
|
|
430
|
+
|
|
431
|
+
if File.exist?(File.join(self.class.source_root, template_name))
|
|
432
|
+
template template_name, spec_path
|
|
433
|
+
else
|
|
434
|
+
say "Warning: Spec template not found: #{template_name}", :yellow
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def generate_spec_files
|
|
439
|
+
# Generate builder spec (covers entity validation too)
|
|
440
|
+
builder_spec_path = File.join(RiderKick.configuration.domains_path, 'builders', "#{@variable_subject}_spec.rb")
|
|
441
|
+
template 'domains/core/builders/builder_spec.rb.tt', builder_spec_path
|
|
442
|
+
|
|
443
|
+
# Generate model spec
|
|
444
|
+
generate_model_spec
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def generate_model_spec
|
|
448
|
+
model_spec_path = model_file_path(@model_class, @variable_subject).gsub('.rb', '_spec.rb')
|
|
449
|
+
# Place spec alongside the model file (same directory)
|
|
450
|
+
# model_spec_path sudah berisi path lengkap ke app/models/.../_spec.rb
|
|
451
|
+
|
|
452
|
+
template 'models/model_spec.rb.tt', model_spec_path
|
|
453
|
+
end
|
|
139
454
|
end
|
|
140
455
|
end
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require 'rails/generators'
|
|
4
4
|
require 'tmpdir'
|
|
5
5
|
require 'active_support/inflector'
|
|
6
|
-
require 'ostruct'
|
|
7
6
|
require 'fileutils'
|
|
8
7
|
require 'generators/rider_kick/scaffold_generator'
|
|
9
8
|
|
|
@@ -13,13 +12,13 @@ RSpec.describe 'rider_kick:scaffold builder (uploaders)' do
|
|
|
13
12
|
it 'menulis mapping uploader: single (has_one) & multiple (has_many) ke builder' 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
|
|
|
25
24
|
File.write('app/models/models/user.rb', "class Models::User < ApplicationRecord; end\n")
|
|
@@ -29,7 +28,9 @@ RSpec.describe 'rider_kick:scaffold builder (uploaders)' do
|
|
|
29
28
|
model: Models::User
|
|
30
29
|
resource_name: users
|
|
31
30
|
actor: owner
|
|
32
|
-
|
|
31
|
+
resource_owner_id: account_id
|
|
32
|
+
resource_owner: account
|
|
33
|
+
uploaders: [{ name: 'avatar', type: 'single' }, { name: 'images', type: 'multiple' }]
|
|
33
34
|
search_able: []
|
|
34
35
|
domains:
|
|
35
36
|
action_list: { use_case: { contract: [] } }
|
|
@@ -37,17 +38,121 @@ RSpec.describe 'rider_kick:scaffold builder (uploaders)' do
|
|
|
37
38
|
action_create: { use_case: { contract: [] } }
|
|
38
39
|
action_update: { use_case: { contract: [] } }
|
|
39
40
|
action_destroy: { use_case: { contract: [] } }
|
|
40
|
-
entity: {
|
|
41
|
+
entity: { db_attributes: [id, created_at, updated_at] }
|
|
41
42
|
YAML
|
|
42
43
|
|
|
43
44
|
klass.new(['users']).generate_use_case
|
|
44
45
|
|
|
45
|
-
builder = File.read('
|
|
46
|
+
builder = File.read(RiderKick.configuration.domains_path + '/builders/user.rb')
|
|
46
47
|
# singular -> satu URL string
|
|
47
|
-
expect(builder).to include('
|
|
48
|
+
expect(builder).to include('def with_avatar_url(model)')
|
|
49
|
+
expect(builder).to include('model.avatar.url')
|
|
48
50
|
# plural -> array dengan helper build_assets
|
|
49
|
-
expect(builder).to include('
|
|
50
|
-
expect(builder).to include('
|
|
51
|
+
expect(builder).to include('def with_images_urls(model)')
|
|
52
|
+
expect(builder).to include('model.images.map')
|
|
53
|
+
expect(builder).to include('attachment.url')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'tidak menambahkan duplikasi has_one_attached atau has_many_attached jika sudah ada' do
|
|
59
|
+
Dir.mktmpdir do |dir|
|
|
60
|
+
Dir.chdir(dir) do
|
|
61
|
+
FileUtils.mkdir_p([
|
|
62
|
+
RiderKick.configuration.domains_path + '/core/use_cases',
|
|
63
|
+
RiderKick.configuration.domains_path + '/core/repositories',
|
|
64
|
+
RiderKick.configuration.domains_path + '/core/builders',
|
|
65
|
+
RiderKick.configuration.domains_path + '/core/entities',
|
|
66
|
+
'app/models/models',
|
|
67
|
+
'db/structures'
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
# Model dengan attachment yang sudah ada
|
|
71
|
+
File.write('app/models/models/user.rb', <<~RUBY)
|
|
72
|
+
class Models::User < ApplicationRecord
|
|
73
|
+
has_one_attached :avatar, dependent: :purge
|
|
74
|
+
has_many_attached :images, dependent: :purge
|
|
75
|
+
end
|
|
76
|
+
RUBY
|
|
77
|
+
|
|
78
|
+
File.write('db/structures/users_structure.yaml', <<~YAML)
|
|
79
|
+
model: Models::User
|
|
80
|
+
resource_name: users
|
|
81
|
+
actor: owner
|
|
82
|
+
resource_owner_id: account_id
|
|
83
|
+
resource_owner: account
|
|
84
|
+
uploaders: [{ name: 'avatar', type: 'single' }, { name: 'images', type: 'multiple' }]
|
|
85
|
+
search_able: []
|
|
86
|
+
domains:
|
|
87
|
+
action_list: { use_case: { contract: [] } }
|
|
88
|
+
action_fetch_by_id: { use_case: { contract: [] } }
|
|
89
|
+
action_create: { use_case: { contract: [] } }
|
|
90
|
+
action_update: { use_case: { contract: [] } }
|
|
91
|
+
action_destroy: { use_case: { contract: [] } }
|
|
92
|
+
entity: { db_attributes: [id, created_at, updated_at] }
|
|
93
|
+
YAML
|
|
94
|
+
|
|
95
|
+
klass.new(['users']).generate_use_case
|
|
96
|
+
|
|
97
|
+
model_content = File.read('app/models/models/user.rb')
|
|
98
|
+
|
|
99
|
+
# Pastikan tidak ada duplikasi
|
|
100
|
+
avatar_count = model_content.scan(/has_one_attached\s+:avatar\b/).count
|
|
101
|
+
images_count = model_content.scan(/has_many_attached\s+:images\b/).count
|
|
102
|
+
|
|
103
|
+
expect(avatar_count).to eq(1), "Expected has_one_attached :avatar to appear only once, but found #{avatar_count} times"
|
|
104
|
+
expect(images_count).to eq(1), "Expected has_many_attached :images to appear only once, but found #{images_count} times"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'tetap mendeteksi attachment yang sudah ada dengan format berbeda' do
|
|
110
|
+
Dir.mktmpdir do |dir|
|
|
111
|
+
Dir.chdir(dir) do
|
|
112
|
+
FileUtils.mkdir_p([
|
|
113
|
+
RiderKick.configuration.domains_path + '/core/use_cases',
|
|
114
|
+
RiderKick.configuration.domains_path + '/core/repositories',
|
|
115
|
+
RiderKick.configuration.domains_path + '/core/builders',
|
|
116
|
+
RiderKick.configuration.domains_path + '/core/entities',
|
|
117
|
+
'app/models/models',
|
|
118
|
+
'db/structures'
|
|
119
|
+
])
|
|
120
|
+
|
|
121
|
+
# Model dengan attachment yang sudah ada dengan format berbeda (tanpa dependent)
|
|
122
|
+
File.write('app/models/models/user.rb', <<~RUBY)
|
|
123
|
+
class Models::User < ApplicationRecord
|
|
124
|
+
has_one_attached :avatar
|
|
125
|
+
has_many_attached :images
|
|
126
|
+
end
|
|
127
|
+
RUBY
|
|
128
|
+
|
|
129
|
+
File.write('db/structures/users_structure.yaml', <<~YAML)
|
|
130
|
+
model: Models::User
|
|
131
|
+
resource_name: users
|
|
132
|
+
actor: owner
|
|
133
|
+
resource_owner_id: account_id
|
|
134
|
+
resource_owner: account
|
|
135
|
+
uploaders: [{ name: 'avatar', type: 'single' }, { name: 'images', type: 'multiple' }]
|
|
136
|
+
search_able: []
|
|
137
|
+
domains:
|
|
138
|
+
action_list: { use_case: { contract: [] } }
|
|
139
|
+
action_fetch_by_id: { use_case: { contract: [] } }
|
|
140
|
+
action_create: { use_case: { contract: [] } }
|
|
141
|
+
action_update: { use_case: { contract: [] } }
|
|
142
|
+
action_destroy: { use_case: { contract: [] } }
|
|
143
|
+
entity: { db_attributes: [id, created_at, updated_at] }
|
|
144
|
+
YAML
|
|
145
|
+
|
|
146
|
+
klass.new(['users']).generate_use_case
|
|
147
|
+
|
|
148
|
+
model_content = File.read('app/models/models/user.rb')
|
|
149
|
+
|
|
150
|
+
# Pastikan tidak ada duplikasi meskipun format berbeda
|
|
151
|
+
avatar_count = model_content.scan(/has_one_attached\s+:avatar\b/).count
|
|
152
|
+
images_count = model_content.scan(/has_many_attached\s+:images\b/).count
|
|
153
|
+
|
|
154
|
+
expect(avatar_count).to eq(1), "Expected has_one_attached :avatar to appear only once, but found #{avatar_count} times"
|
|
155
|
+
expect(images_count).to eq(1), "Expected has_many_attached :images to appear only once, but found #{images_count} times"
|
|
51
156
|
end
|
|
52
157
|
end
|
|
53
158
|
end
|