dscf-core 0.1.7 → 0.1.8
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/app/controllers/concerns/dscf/core/json_response.rb +12 -1
- data/app/controllers/concerns/dscf/core/reviewable_controller.rb +345 -0
- data/app/controllers/dscf/core/auth_controller.rb +19 -14
- data/app/models/concerns/dscf/core/reviewable_model.rb +31 -0
- data/app/models/dscf/core/review.rb +22 -0
- data/app/models/dscf/core/role.rb +8 -0
- data/app/models/dscf/core/user.rb +8 -0
- data/app/serializers/dscf/core/address_serializer.rb +10 -0
- data/app/serializers/dscf/core/business_serializer.rb +10 -0
- data/app/serializers/dscf/core/business_type_serializer.rb +9 -0
- data/app/serializers/dscf/core/review_serializer.rb +16 -0
- data/app/serializers/dscf/core/role_serializer.rb +9 -0
- data/app/serializers/dscf/core/user_auth_serializer.rb +10 -0
- data/app/serializers/dscf/core/user_profile_serializer.rb +12 -0
- data/app/serializers/dscf/core/user_role_serializer.rb +10 -0
- data/app/serializers/dscf/core/user_serializer.rb +14 -0
- data/db/migrate/20250926102025_create_dscf_core_reviews.rb +14 -0
- data/lib/dscf/core/version.rb +1 -1
- data/lib/generators/common/USAGE +39 -0
- data/lib/generators/common/common_generator.rb +579 -0
- data/lib/generators/common/templates/controller.rb.erb +75 -0
- data/lib/generators/common/templates/request_spec.rb.erb +33 -0
- data/lib/generators/common/templates/serializer.rb.erb +43 -0
- data/spec/factories/dscf/core/reviews.rb +11 -0
- metadata +21 -2
@@ -0,0 +1,39 @@
|
|
1
|
+
This generator creates a fully functional API resource for a given model using DSCF conventions.
|
2
|
+
|
3
|
+
Usage:
|
4
|
+
rails generate common NAMESPACE::ModelName [options]
|
5
|
+
|
6
|
+
Options:
|
7
|
+
--only COMPONENT1 COMPONENT2 Only generate specific components
|
8
|
+
--skip COMPONENT1 COMPONENT2 Skip generating specific components
|
9
|
+
|
10
|
+
Available components: controller, serializer, request_spec, routes, locales, model
|
11
|
+
|
12
|
+
Examples:
|
13
|
+
rails generate common Dscf::Core::User
|
14
|
+
rails generate common UserProfile
|
15
|
+
rails generate common Billing::Invoice
|
16
|
+
rails generate common Dscf::Core::Role --only serializer request_spec
|
17
|
+
rails generate common UserProfile --skip routes locales
|
18
|
+
|
19
|
+
Note: Use space-separated values for --only/--skip options.
|
20
|
+
Do not use multiple --only/--skip flags as they will override each other.
|
21
|
+
|
22
|
+
Automatic Serializer Generation:
|
23
|
+
When generating a serializer, the generator analyzes model associations and
|
24
|
+
prompts to create missing serializers for associated models. This helps ensure
|
25
|
+
complete API serialization without manual tracking of dependencies.
|
26
|
+
|
27
|
+
• Detects missing serializers for belongs_to, has_many, has_one associations
|
28
|
+
• Interactive prompts with clear feedback
|
29
|
+
• Recursion protection prevents infinite loops in circular associations
|
30
|
+
• Can be declined to generate serializers manually later
|
31
|
+
|
32
|
+
Notes:
|
33
|
+
• If the model file exists, `ransackable_attributes` and `ransackable_associations`
|
34
|
+
will be added just above any `private` section (or at the bottom of the class).
|
35
|
+
• The generator uses introspection to avoid adding insecure attributes (e.g. password fields).
|
36
|
+
• Existing route entries, ransackable methods, or locales are not duplicated.
|
37
|
+
• If a model file is missing, serializer and ransack methods are skipped.
|
38
|
+
|
39
|
+
This generator is engine-aware and respects module namespaces.
|
@@ -0,0 +1,579 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/named_base"
|
3
|
+
|
4
|
+
class CommonGenerator < Rails::Generators::NamedBase
|
5
|
+
VALID_COMPONENTS = %w[
|
6
|
+
controller
|
7
|
+
serializer
|
8
|
+
request_spec
|
9
|
+
routes
|
10
|
+
locales
|
11
|
+
model
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
source_root File.expand_path("templates", __dir__)
|
15
|
+
desc File.read(File.expand_path("USAGE", __dir__))
|
16
|
+
|
17
|
+
class_option :only, type: :array, default: [],
|
18
|
+
desc: "Only generate specific components (controller, serializer, request_spec, routes, locales, model)"
|
19
|
+
class_option :skip, type: :array, default: [],
|
20
|
+
desc: "Skip generating specific components (controller, serializer, request_spec, routes, locales, model)"
|
21
|
+
class_option :_recursive, type: :boolean, default: false,
|
22
|
+
desc: "Internal flag to prevent recursive serializer generation prompts"
|
23
|
+
|
24
|
+
def validate_generator_options
|
25
|
+
validate_options!
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_options!
|
29
|
+
invalid_only = options[:only] - VALID_COMPONENTS
|
30
|
+
invalid_skip = options[:skip] - VALID_COMPONENTS
|
31
|
+
|
32
|
+
if invalid_only.any?
|
33
|
+
raise Thor::Error,
|
34
|
+
"Invalid values for --only: #{invalid_only.join(', ')}. Allowed values: #{VALID_COMPONENTS.join(', ')}"
|
35
|
+
end
|
36
|
+
|
37
|
+
return unless invalid_skip.any?
|
38
|
+
|
39
|
+
raise Thor::Error,
|
40
|
+
"Invalid values for --skip: #{invalid_skip.join(', ')}. Allowed values: #{VALID_COMPONENTS.join(', ')}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_controller
|
44
|
+
return unless should_generate?("controller")
|
45
|
+
|
46
|
+
template "controller.rb.erb", controller_file_path
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_serializer
|
50
|
+
return unless should_generate?("serializer")
|
51
|
+
return unless model_exists?
|
52
|
+
|
53
|
+
# Only check for missing serializers when creating (not destroying) and not in recursive generation
|
54
|
+
check_and_prompt_for_missing_serializers unless behavior == :revoke || recursive_generation?
|
55
|
+
|
56
|
+
template "serializer.rb.erb", serializer_file_path
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_request_spec
|
60
|
+
return unless should_generate?("request_spec")
|
61
|
+
|
62
|
+
template "request_spec.rb.erb", request_spec_file_path
|
63
|
+
end
|
64
|
+
|
65
|
+
def update_model_with_ransackable_methods
|
66
|
+
return unless should_generate?("model")
|
67
|
+
return unless model_exists?
|
68
|
+
return unless model_file_exists?
|
69
|
+
|
70
|
+
add_ransackable_methods_to_model
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_routes
|
74
|
+
return unless should_generate?("routes")
|
75
|
+
|
76
|
+
route_definition = " resources :#{route_name}"
|
77
|
+
|
78
|
+
if File.exist?(routes_file_path)
|
79
|
+
# Check if route already exists
|
80
|
+
unless File.read(routes_file_path).include?("resources :#{route_name}")
|
81
|
+
# Try to inject after engine routes draw or Rails routes draw
|
82
|
+
routes_content = File.read(routes_file_path)
|
83
|
+
if routes_content.include?("Engine.routes.draw do")
|
84
|
+
inject_into_file routes_file_path, "#{route_definition}\n", after: /\.routes\.draw do\n/
|
85
|
+
elsif routes_content.include?("Rails.application.routes.draw do")
|
86
|
+
inject_into_file routes_file_path, "#{route_definition}\n", after: /Rails\.application\.routes\.draw do\n/
|
87
|
+
else
|
88
|
+
# Fallback: append to the end before the last 'end'
|
89
|
+
inject_into_file routes_file_path, "#{route_definition}\n", before: /^end\s*$/
|
90
|
+
end
|
91
|
+
end
|
92
|
+
else
|
93
|
+
create_file routes_file_path, <<~ROUTES
|
94
|
+
Rails.application.routes.draw do
|
95
|
+
#{route_definition}
|
96
|
+
end
|
97
|
+
ROUTES
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def add_locale_entries
|
102
|
+
return unless should_generate?("locales")
|
103
|
+
|
104
|
+
locale_entries = generate_locale_entries
|
105
|
+
indented_entries = indent_locale_block(locale_entries, 2)
|
106
|
+
|
107
|
+
if File.exist?(locale_file_path)
|
108
|
+
model_key = model_class_name.underscore
|
109
|
+
append_to_file locale_file_path, "\n\n#{indented_entries}" unless File.read(locale_file_path).include?("#{model_key}:")
|
110
|
+
else
|
111
|
+
create_file locale_file_path, <<~LOCALE
|
112
|
+
en:
|
113
|
+
#{indented_entries}
|
114
|
+
LOCALE
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def recursive_generation?
|
121
|
+
options[:_recursive] == true
|
122
|
+
end
|
123
|
+
|
124
|
+
def should_generate?(component)
|
125
|
+
return false if options[:skip].include?(component)
|
126
|
+
return true if options[:only].empty?
|
127
|
+
|
128
|
+
options[:only].include?(component)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Name parsing methods
|
132
|
+
def resource_class_name
|
133
|
+
@resource_class_name ||= name
|
134
|
+
end
|
135
|
+
|
136
|
+
def controller_class_name
|
137
|
+
"#{model_class_name.pluralize}Controller"
|
138
|
+
end
|
139
|
+
|
140
|
+
def model_class_name
|
141
|
+
if resource_class_name.include?("::")
|
142
|
+
resource_class_name.split("::").last.singularize
|
143
|
+
else
|
144
|
+
resource_class_name.singularize
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def controller_name
|
149
|
+
model_class_name.pluralize.underscore
|
150
|
+
end
|
151
|
+
|
152
|
+
def route_name
|
153
|
+
controller_name
|
154
|
+
end
|
155
|
+
|
156
|
+
def factory_name
|
157
|
+
resource_class_name.underscore.tr("/", "_")
|
158
|
+
end
|
159
|
+
|
160
|
+
# File path methods
|
161
|
+
def namespace_path
|
162
|
+
if resource_class_name.include?("::")
|
163
|
+
resource_class_name.deconstantize.underscore.gsub("::", "/")
|
164
|
+
else
|
165
|
+
""
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def controller_file_path
|
170
|
+
if namespace_path.present?
|
171
|
+
"app/controllers/#{namespace_path}/#{controller_name}_controller.rb"
|
172
|
+
else
|
173
|
+
"app/controllers/#{controller_name}_controller.rb"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def serializer_file_path
|
178
|
+
if namespace_path.present?
|
179
|
+
"app/serializers/#{namespace_path}/#{model_class_name.underscore}_serializer.rb"
|
180
|
+
else
|
181
|
+
"app/serializers/#{model_class_name.underscore}_serializer.rb"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def request_spec_file_path
|
186
|
+
if namespace_path.present?
|
187
|
+
"spec/requests/#{namespace_path}/#{controller_name}_spec.rb"
|
188
|
+
else
|
189
|
+
"spec/requests/#{controller_name}_spec.rb"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def routes_file_path
|
194
|
+
"config/routes.rb"
|
195
|
+
end
|
196
|
+
|
197
|
+
def locale_file_path
|
198
|
+
"config/locales/en.yml"
|
199
|
+
end
|
200
|
+
|
201
|
+
def model_file_path
|
202
|
+
if namespace_path.present?
|
203
|
+
"app/models/#{namespace_path}/#{model_class_name.underscore}.rb"
|
204
|
+
else
|
205
|
+
"app/models/#{model_class_name.underscore}.rb"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Model introspection methods
|
210
|
+
def model_exists?
|
211
|
+
resource_class_name.constantize
|
212
|
+
true
|
213
|
+
rescue NameError
|
214
|
+
raise Thor::Error, <<~ERROR if should_generate?("serializer") || should_generate?("model")
|
215
|
+
Model `#{resource_class_name}` not found.
|
216
|
+
Make sure it exists, or skip model-dependent components using:
|
217
|
+
--skip model serializer
|
218
|
+
ERROR
|
219
|
+
|
220
|
+
false
|
221
|
+
end
|
222
|
+
|
223
|
+
def model_file_exists?
|
224
|
+
File.exist?(model_file_path)
|
225
|
+
end
|
226
|
+
|
227
|
+
def model_class
|
228
|
+
@model_class ||= resource_class_name.constantize
|
229
|
+
end
|
230
|
+
|
231
|
+
def model_attributes
|
232
|
+
return [] unless model_exists?
|
233
|
+
|
234
|
+
excluded_columns = %w[id created_at updated_at]
|
235
|
+
|
236
|
+
model_class.column_names.reject do |column|
|
237
|
+
column.end_with?("_id") || excluded_columns.include?(column)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def model_associations
|
242
|
+
return [] unless model_exists?
|
243
|
+
|
244
|
+
model_class.reflect_on_all_associations.map do |association|
|
245
|
+
# Get the actual class name by trying to constantize it
|
246
|
+
# This handles cases where Rails infers the class name based on the association name
|
247
|
+
actual_class_name = begin
|
248
|
+
association.klass.name
|
249
|
+
rescue NameError
|
250
|
+
# If the class can't be loaded, fall back to the association's class_name
|
251
|
+
association.class_name
|
252
|
+
end
|
253
|
+
|
254
|
+
{
|
255
|
+
name: association.name,
|
256
|
+
macro: association.macro,
|
257
|
+
class_name: actual_class_name
|
258
|
+
}
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Ransackable methods
|
263
|
+
def sensitive_attributes
|
264
|
+
%w[
|
265
|
+
password password_digest password_confirmation
|
266
|
+
token api_key secret_key access_token refresh_token
|
267
|
+
salt encrypted_password reset_password_token
|
268
|
+
confirmation_token unlock_token authentication_token
|
269
|
+
otp_secret_key single_access_token perishable_token
|
270
|
+
remember_token remember_created_at current_sign_in_at
|
271
|
+
last_sign_in_at current_sign_in_ip last_sign_in_ip
|
272
|
+
failed_attempts locked_at
|
273
|
+
]
|
274
|
+
end
|
275
|
+
|
276
|
+
def safe_ransackable_attributes
|
277
|
+
return [] unless model_exists?
|
278
|
+
|
279
|
+
all_attributes = model_class.column_names
|
280
|
+
all_attributes.reject do |attr|
|
281
|
+
sensitive_attributes.any? { |sensitive| attr.downcase.include?(sensitive.downcase) }
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def safe_ransackable_associations
|
286
|
+
return [] unless model_exists?
|
287
|
+
|
288
|
+
model_associations.map { |assoc| assoc[:name].to_s }
|
289
|
+
end
|
290
|
+
|
291
|
+
def model_has_ransackable_methods?
|
292
|
+
return false unless model_file_exists?
|
293
|
+
|
294
|
+
model_content = File.read(model_file_path)
|
295
|
+
|
296
|
+
# Check for existing methods
|
297
|
+
has_attributes_method = model_content.match?(/def\s+self\.ransackable_attributes(\s*\(.*\))?\s*$/)
|
298
|
+
has_associations_method = model_content.match?(/def\s+self\.ransackable_associations(\s*\(.*\))?\s*$/)
|
299
|
+
|
300
|
+
has_attributes_method || has_associations_method
|
301
|
+
end
|
302
|
+
|
303
|
+
def add_ransackable_methods_to_model
|
304
|
+
return unless model_file_exists?
|
305
|
+
return if model_has_ransackable_methods?
|
306
|
+
|
307
|
+
model_content = File.read(model_file_path)
|
308
|
+
|
309
|
+
# Determine the appropriate indentation by looking at existing methods
|
310
|
+
indentation = detect_model_indentation(model_content)
|
311
|
+
|
312
|
+
ransackable_methods = <<~RUBY
|
313
|
+
|
314
|
+
#{indentation}def self.ransackable_attributes(auth_object = nil)
|
315
|
+
#{indentation} %w[#{safe_ransackable_attributes.join(' ')}]
|
316
|
+
#{indentation}end
|
317
|
+
|
318
|
+
#{indentation}def self.ransackable_associations(auth_object = nil)
|
319
|
+
#{indentation} %w[#{safe_ransackable_associations.join(' ')}]
|
320
|
+
#{indentation}end
|
321
|
+
RUBY
|
322
|
+
|
323
|
+
# Strategy 1: Place just before 'private' if it exists and is properly indented
|
324
|
+
if model_content.match?(/^\s*private\s*$/)
|
325
|
+
inject_into_file model_file_path, ransackable_methods, before: /^\s*private\s*$/
|
326
|
+
# Strategy 2: Place just before 'protected' if it exists (but no private)
|
327
|
+
elsif model_content.match?(/^\s*protected\s*$/)
|
328
|
+
inject_into_file model_file_path, ransackable_methods, before: /^\s*protected\s*$/
|
329
|
+
# Strategy 3: Place before the last 'end' of the class (try to find the class end)
|
330
|
+
else
|
331
|
+
# Look for the last 'end' that closes the class
|
332
|
+
inject_into_file model_file_path, ransackable_methods, before: /^#{indentation.chomp}end\s*$|^end\s*$/
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def detect_model_indentation(model_content)
|
337
|
+
# Look for existing method definitions to determine indentation
|
338
|
+
method_match = model_content.match(/^(\s+)def\s+/)
|
339
|
+
if method_match
|
340
|
+
method_match[1]
|
341
|
+
else
|
342
|
+
# Fallback: look for any line that's indented within the class
|
343
|
+
line_match = model_content.match(/^(\s+)\w+/)
|
344
|
+
if line_match
|
345
|
+
line_match[1]
|
346
|
+
else
|
347
|
+
" " # Default to 2 spaces
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def serializer_attributes
|
353
|
+
return ":id, :created_at, :updated_at" unless model_exists?
|
354
|
+
|
355
|
+
base_attrs = %w[id]
|
356
|
+
custom_attrs = model_attributes.reject { |attr| is_sensitive_attribute?(attr) }
|
357
|
+
timestamp_attrs = %w[created_at updated_at]
|
358
|
+
|
359
|
+
all_attrs = (base_attrs + custom_attrs + timestamp_attrs)
|
360
|
+
":#{all_attrs.join(', :')}"
|
361
|
+
end
|
362
|
+
|
363
|
+
def serializer_associations
|
364
|
+
return {} unless model_exists?
|
365
|
+
|
366
|
+
grouped_associations = {}
|
367
|
+
|
368
|
+
model_associations.group_by { |assoc| assoc[:macro] }.each do |macro, associations|
|
369
|
+
grouped_associations[macro] = associations.map do |association|
|
370
|
+
# Only try to find/suggest serializers when creating (not destroying)
|
371
|
+
serializer_name = if behavior == :revoke
|
372
|
+
# For destroy mode, just use the basic serializer name without checking
|
373
|
+
if association[:class_name].include?("::")
|
374
|
+
namespace_parts = association[:class_name].split("::")
|
375
|
+
"#{namespace_parts.last}Serializer"
|
376
|
+
else
|
377
|
+
"#{association[:class_name]}Serializer"
|
378
|
+
end
|
379
|
+
else
|
380
|
+
find_or_suggest_serializer(association[:class_name])
|
381
|
+
end
|
382
|
+
|
383
|
+
{
|
384
|
+
name: association[:name],
|
385
|
+
class_name: association[:class_name],
|
386
|
+
serializer: serializer_name
|
387
|
+
}
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
grouped_associations
|
392
|
+
end
|
393
|
+
|
394
|
+
# Check if an attribute is sensitive and should be excluded from serializers
|
395
|
+
def is_sensitive_attribute?(attr_name)
|
396
|
+
sensitive_attributes.any? { |sensitive| attr_name.downcase.include?(sensitive.downcase) }
|
397
|
+
end
|
398
|
+
|
399
|
+
# Find existing serializer or suggest a name for one
|
400
|
+
def find_or_suggest_serializer(class_name)
|
401
|
+
# Handle namespaced classes
|
402
|
+
if class_name.include?("::")
|
403
|
+
namespace_parts = class_name.split("::")
|
404
|
+
model_name = namespace_parts.last
|
405
|
+
namespace = namespace_parts[0..-2].join("::")
|
406
|
+
suggested_name = "#{model_name}Serializer"
|
407
|
+
serializer_class_name = "#{namespace}::#{suggested_name}"
|
408
|
+
else
|
409
|
+
suggested_name = "#{class_name}Serializer"
|
410
|
+
serializer_class_name = suggested_name
|
411
|
+
end
|
412
|
+
|
413
|
+
# Check if serializer file exists (more reliable than constantize during generation)
|
414
|
+
serializer_file = serializer_file_path_for_class(class_name)
|
415
|
+
if File.exist?(serializer_file)
|
416
|
+
serializer_class_name
|
417
|
+
else
|
418
|
+
# Return suggested name, will be used in template
|
419
|
+
suggested_name
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
# Check for missing serializers and prompt user
|
424
|
+
def check_and_prompt_for_missing_serializers
|
425
|
+
missing_serializers = find_missing_serializers
|
426
|
+
return if missing_serializers.empty?
|
427
|
+
return unless behavior == :invoke # Only prompt on generate, not destroy
|
428
|
+
|
429
|
+
say "\n🔍 Detected associations without serializers:", :yellow
|
430
|
+
missing_serializers.each do |missing|
|
431
|
+
say " • #{missing[:association_name]} (#{missing[:class_name]}) -> #{missing[:suggested_serializer]}"
|
432
|
+
end
|
433
|
+
|
434
|
+
say "\n🛠️ Would you like to generate the missing serializers now? (y/n)", :green
|
435
|
+
if yes?("")
|
436
|
+
say "\n🚀 Generating missing serializers...\n"
|
437
|
+
generate_missing_serializers_safely(missing_serializers)
|
438
|
+
say "✅ All missing serializers have been generated.\n", :green
|
439
|
+
else
|
440
|
+
say "\n🛑 Skipping serializer generation.\n", :yellow
|
441
|
+
say "ℹ️ You can generate them later with:", :blue
|
442
|
+
missing_serializers.each do |missing|
|
443
|
+
say " rails generate common #{missing[:class_name]} --only serializer"
|
444
|
+
end
|
445
|
+
say ""
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# Generate missing serializers safely without recursion
|
450
|
+
def generate_missing_serializers_safely(missing_serializers)
|
451
|
+
missing_serializers.each do |missing|
|
452
|
+
serializer_path = serializer_file_path_for_class(missing[:class_name])
|
453
|
+
|
454
|
+
if File.exist?(serializer_path)
|
455
|
+
say " exists #{serializer_path}", :blue
|
456
|
+
next
|
457
|
+
end
|
458
|
+
begin
|
459
|
+
args = [missing[:class_name]]
|
460
|
+
opts = {only: ["serializer"], _recursive: true} # Add your recursive flag if needed
|
461
|
+
|
462
|
+
generator = self.class.new(args, opts, {
|
463
|
+
behavior: :invoke,
|
464
|
+
destination_root: destination_root,
|
465
|
+
shell: shell # Uses current shell; can customize if you want silence
|
466
|
+
})
|
467
|
+
|
468
|
+
generator.invoke_all
|
469
|
+
|
470
|
+
say " ✅ Created #{missing[:suggested_serializer]}"
|
471
|
+
rescue StandardError => e
|
472
|
+
say " ❌ Failed to create #{missing[:suggested_serializer]}: #{e.message}", :red
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
# Find all missing serializers for associations
|
478
|
+
def find_missing_serializers
|
479
|
+
missing = []
|
480
|
+
|
481
|
+
serializer_associations.each_value do |associations|
|
482
|
+
associations.each do |association|
|
483
|
+
class_name = association[:class_name]
|
484
|
+
|
485
|
+
next if class_name.start_with?("ActiveStorage::")
|
486
|
+
|
487
|
+
# Check if serializer file exists
|
488
|
+
serializer_file = serializer_file_path_for_class(class_name)
|
489
|
+
|
490
|
+
next if File.exist?(serializer_file)
|
491
|
+
|
492
|
+
# Try to find the serializer
|
493
|
+
if class_name.include?("::")
|
494
|
+
namespace_parts = class_name.split("::")
|
495
|
+
model_name = namespace_parts.last
|
496
|
+
namespace = namespace_parts[0..-2].join("::")
|
497
|
+
suggested_serializer = "#{model_name}Serializer"
|
498
|
+
full_serializer_name = "#{namespace}::#{suggested_serializer}"
|
499
|
+
else
|
500
|
+
suggested_serializer = "#{class_name}Serializer"
|
501
|
+
full_serializer_name = suggested_serializer
|
502
|
+
end
|
503
|
+
|
504
|
+
missing << {
|
505
|
+
association_name: association[:name],
|
506
|
+
class_name: class_name,
|
507
|
+
suggested_serializer: suggested_serializer,
|
508
|
+
full_serializer_name: full_serializer_name
|
509
|
+
}
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
missing.uniq { |m| m[:class_name] }
|
514
|
+
end
|
515
|
+
|
516
|
+
# Get serializer file path for a given class name
|
517
|
+
def serializer_file_path_for_class(class_name)
|
518
|
+
if class_name.include?("::")
|
519
|
+
namespace_parts = class_name.split("::")
|
520
|
+
model_name = namespace_parts.last
|
521
|
+
namespace_path = namespace_parts[0..-2].join("/").underscore
|
522
|
+
"app/serializers/#{namespace_path}/#{model_name.underscore}_serializer.rb"
|
523
|
+
else
|
524
|
+
"app/serializers/#{class_name.underscore}_serializer.rb"
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
# Controller namespace methods
|
529
|
+
def controller_namespace
|
530
|
+
resource_class_name.include?("::") ? resource_class_name.deconstantize : nil
|
531
|
+
end
|
532
|
+
|
533
|
+
def controller_namespace_modules
|
534
|
+
return [] unless controller_namespace
|
535
|
+
|
536
|
+
controller_namespace.split("::")
|
537
|
+
end
|
538
|
+
|
539
|
+
# Locale generation
|
540
|
+
def generate_locale_entries
|
541
|
+
model_key = model_class_name.underscore
|
542
|
+
|
543
|
+
<<~LOCALE.chomp
|
544
|
+
#{model_key}:
|
545
|
+
success:
|
546
|
+
index: "#{model_class_name.pluralize.humanize} retrieved successfully"
|
547
|
+
show: "#{model_class_name.humanize} details retrieved successfully"
|
548
|
+
create: "#{model_class_name.humanize} created successfully"
|
549
|
+
update: "#{model_class_name.humanize} updated successfully"
|
550
|
+
errors:
|
551
|
+
index: "Failed to retrieve #{model_class_name.pluralize.humanize}"
|
552
|
+
show: "Failed to retrieve #{model_class_name.humanize} details"
|
553
|
+
create: "Failed to create #{model_class_name.humanize.downcase}"
|
554
|
+
update: "Failed to update #{model_class_name.humanize.downcase}"
|
555
|
+
LOCALE
|
556
|
+
end
|
557
|
+
|
558
|
+
def indent_locale_block(yaml_string, spaces)
|
559
|
+
indent = " " * spaces
|
560
|
+
yaml_string.lines.map { |line| "#{indent}#{line}" }.join
|
561
|
+
end
|
562
|
+
|
563
|
+
# Spec helper methods
|
564
|
+
def spec_describe_name
|
565
|
+
if namespace_path.present?
|
566
|
+
"'#{namespace_path.camelize}::#{controller_class_name}'"
|
567
|
+
else
|
568
|
+
"'#{controller_class_name}'"
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
def spec_url_helper
|
573
|
+
if namespace_path.present?
|
574
|
+
"#{namespace_path.underscore.tr('/', '_')}_#{controller_name}"
|
575
|
+
else
|
576
|
+
controller_name
|
577
|
+
end
|
578
|
+
end
|
579
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
<% if controller_namespace_modules.any? -%>
|
2
|
+
<% controller_namespace_modules.each do |mod| -%>
|
3
|
+
module <%= mod %>
|
4
|
+
<% end -%>
|
5
|
+
class <%= controller_class_name %> < ApplicationController
|
6
|
+
include Dscf::Core::Common
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def model_params
|
11
|
+
params.require(:<%= model_class_name.underscore %>).permit(
|
12
|
+
# TODO: Add your permitted parameters here
|
13
|
+
# Example: :name, :email, :status
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Override to define associations to eager load
|
18
|
+
def eager_loaded_associations
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Override to define allowed columns for ordering/pagination
|
23
|
+
def allowed_order_columns
|
24
|
+
%w[id created_at updated_at]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Override to define serializer includes for different actions
|
28
|
+
def default_serializer_includes
|
29
|
+
{
|
30
|
+
default: [],
|
31
|
+
index: [],
|
32
|
+
show: [],
|
33
|
+
create: [],
|
34
|
+
update: []
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
<% controller_namespace_modules.each do |mod| -%>
|
39
|
+
end
|
40
|
+
<% end -%>
|
41
|
+
<% else -%>
|
42
|
+
class <%= controller_class_name %> < ApplicationController
|
43
|
+
include Dscf::Core::Common
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def model_params
|
48
|
+
params.require(:<%= model_class_name.underscore %>).permit(
|
49
|
+
# TODO: Add your permitted parameters here
|
50
|
+
# Example: :name, :email, :status
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Override to define associations to eager load
|
55
|
+
def eager_loaded_associations
|
56
|
+
[]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Override to define allowed columns for ordering/pagination
|
60
|
+
def allowed_order_columns
|
61
|
+
%w[id created_at updated_at]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Override to define serializer includes for different actions
|
65
|
+
def default_serializer_includes
|
66
|
+
{
|
67
|
+
default: [],
|
68
|
+
index: [],
|
69
|
+
show: [],
|
70
|
+
create: [],
|
71
|
+
update: []
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
<% end -%>
|