propel_api 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +59 -0
  3. data/LICENSE +21 -0
  4. data/README.md +320 -0
  5. data/Rakefile +36 -0
  6. data/lib/generators/propel_api/USAGE +8 -0
  7. data/lib/generators/propel_api/controller/controller_generator.rb +208 -0
  8. data/lib/generators/propel_api/core/base.rb +19 -0
  9. data/lib/generators/propel_api/core/configuration_methods.rb +187 -0
  10. data/lib/generators/propel_api/core/named_base.rb +457 -0
  11. data/lib/generators/propel_api/core/path_generation_methods.rb +45 -0
  12. data/lib/generators/propel_api/core/relationship_inferrer.rb +117 -0
  13. data/lib/generators/propel_api/install/install_generator.rb +343 -0
  14. data/lib/generators/propel_api/resource/resource_generator.rb +433 -0
  15. data/lib/generators/propel_api/templates/config/propel_api.rb.tt +149 -0
  16. data/lib/generators/propel_api/templates/controllers/api_controller_graphiti.rb +79 -0
  17. data/lib/generators/propel_api/templates/controllers/api_controller_propel_facets.rb +76 -0
  18. data/lib/generators/propel_api/templates/controllers/example_controller.rb.tt +96 -0
  19. data/lib/generators/propel_api/templates/scaffold/facet_controller_template.rb.tt +80 -0
  20. data/lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt +141 -0
  21. data/lib/generators/propel_api/templates/scaffold/graphiti_controller_template.rb.tt +82 -0
  22. data/lib/generators/propel_api/templates/scaffold/graphiti_model_template.rb.tt +32 -0
  23. data/lib/generators/propel_api/templates/seeds/seeds_template.rb.tt +493 -0
  24. data/lib/generators/propel_api/templates/tests/controller_test_template.rb.tt +485 -0
  25. data/lib/generators/propel_api/templates/tests/fixtures_template.yml.tt +250 -0
  26. data/lib/generators/propel_api/templates/tests/integration_test_template.rb.tt +487 -0
  27. data/lib/generators/propel_api/templates/tests/model_test_template.rb.tt +252 -0
  28. data/lib/generators/propel_api/unpack/unpack_generator.rb +304 -0
  29. data/lib/propel_api.rb +3 -0
  30. metadata +95 -0
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Module providing configuration detection and validation for PropelApi generators
5
+ #
6
+ module PropelApi
7
+ module ConfigurationMethods
8
+
9
+ # Shared class options across all PropelApi generators
10
+ def self.included(base)
11
+ base.class_option :adapter,
12
+ type: :string,
13
+ default: 'propel_facets',
14
+ desc: "Serialization adapter to use: 'propel_facets' or 'graphiti'. Defaults to PropelApi configuration."
15
+
16
+ base.class_option :namespace,
17
+ type: :string,
18
+ default: 'api',
19
+ desc: "API namespace (e.g., 'api', 'admin_api'). Use 'none' for no namespace. Defaults to PropelApi configuration."
20
+
21
+ base.class_option :version,
22
+ type: :string,
23
+ default: 'v1',
24
+ desc: "API version (e.g., 'v1', 'v2'). Use 'none' for no versioning. Defaults to PropelApi configuration."
25
+
26
+ base.class_option :all_attributes,
27
+ type: :boolean,
28
+ default: false,
29
+ desc: "Automatically include all model attributes from file and/or database schema for existing models"
30
+ end
31
+
32
+ protected
33
+
34
+ # Initialize shared PropelApi settings used across all generators
35
+ def initialize_propel_api_settings
36
+ @adapter = determine_adapter
37
+ @api_namespace = determine_api_namespace
38
+ @api_version = determine_api_version
39
+ end
40
+
41
+ # Determine the serialization adapter to use
42
+ # Priority order:
43
+ # 1. Command line option (--adapter)
44
+ # 2. Propel orchestration (when called by Propel installer)
45
+ # 3. PropelApi configuration (if exists)
46
+ # 4. Check if Api::ApiController exists and determine from it
47
+ # 5. Default fallback (propel_facets)
48
+ def determine_adapter
49
+ if options[:adapter].present?
50
+ validate_adapter(options[:adapter])
51
+ return options[:adapter]
52
+ end
53
+
54
+ # Check for Propel orchestration settings (for install_generator)
55
+ if respond_to?(:propel_orchestrated_adapter, true)
56
+ propel_setting = propel_orchestrated_adapter
57
+ if propel_setting.present?
58
+ validate_adapter(propel_setting)
59
+ return propel_setting
60
+ end
61
+ end
62
+
63
+ # Try to read from PropelApi configuration
64
+ begin
65
+ if defined?(PropelApi) && PropelApi.configuration.respond_to?(:adapter)
66
+ config_adapter = PropelApi.configuration.adapter
67
+ if config_adapter.present?
68
+ validate_adapter(config_adapter)
69
+ return config_adapter
70
+ end
71
+ end
72
+ rescue => e
73
+ # Configuration not available, continue to next check
74
+ end
75
+
76
+ # Check if Api::ApiController exists and try to determine engine from it
77
+ api_controller_path = File.join(destination_root, 'app/controllers/api/api_controller.rb')
78
+ if File.exist?(api_controller_path)
79
+ controller_content = File.read(api_controller_path)
80
+ if controller_content.include?('FacetRenderer')
81
+ return 'propel_facets'
82
+ elsif controller_content.include?('Graphiti')
83
+ return 'graphiti'
84
+ end
85
+ end
86
+
87
+ # If no Api::ApiController found, suggest running install first (for resource generators)
88
+ if self.class.name.include?('Generator') && !self.class.name.include?('InstallGenerator')
89
+ say "Warning: No Api::ApiController found. Run 'rails generate propel_api:install' first.", :red
90
+ end
91
+
92
+ # Default fallback
93
+ 'propel_facets'
94
+ end
95
+
96
+ # Validate that the adapter is supported
97
+ def validate_adapter(adapter)
98
+ valid_adapters = %w[propel_facets graphiti]
99
+ unless valid_adapters.include?(adapter)
100
+ raise ArgumentError, "Invalid adapter '#{adapter}'. Valid options: #{valid_adapters.join(', ')}"
101
+ end
102
+ end
103
+
104
+ # Determine the API namespace to use
105
+ # Priority order:
106
+ # 1. Command line option (--namespace)
107
+ # 2. Propel orchestration (when called by Propel installer)
108
+ # 3. PropelApi configuration (if exists)
109
+ # 4. Default fallback ('api')
110
+ def determine_api_namespace
111
+ if options[:namespace] == 'none'
112
+ return nil
113
+ end
114
+
115
+ if options[:namespace].present?
116
+ return options[:namespace]
117
+ end
118
+
119
+ # Check for Propel orchestration settings (for install_generator)
120
+ if respond_to?(:propel_orchestrated_namespace, true)
121
+ propel_setting = propel_orchestrated_namespace
122
+ if propel_setting.present?
123
+ return propel_setting == 'none' ? nil : propel_setting
124
+ end
125
+ end
126
+
127
+ # Try to read from PropelApi configuration
128
+ begin
129
+ if defined?(PropelApi) && PropelApi.configuration.respond_to?(:namespace)
130
+ config_namespace = PropelApi.configuration.namespace
131
+ return config_namespace if config_namespace.present?
132
+ end
133
+ rescue => e
134
+ # Configuration not available, continue to default
135
+ end
136
+
137
+ # Default fallback
138
+ 'api'
139
+ end
140
+
141
+ # Determine the API version to use
142
+ # Priority order:
143
+ # 1. Command line option (--version)
144
+ # 2. Propel orchestration (when called by Propel installer)
145
+ # 3. PropelApi configuration (if exists)
146
+ # 4. Default fallback ('v1')
147
+ def determine_api_version
148
+ if options[:version] == 'none'
149
+ return nil
150
+ end
151
+
152
+ if options[:version].present?
153
+ return options[:version]
154
+ end
155
+
156
+ # Check for Propel orchestration settings (for install_generator)
157
+ if respond_to?(:propel_orchestrated_version, true)
158
+ propel_setting = propel_orchestrated_version
159
+ if propel_setting.present?
160
+ return propel_setting == 'none' ? nil : propel_setting
161
+ end
162
+ end
163
+
164
+ # Try to read from PropelApi configuration
165
+ begin
166
+ if defined?(PropelApi) && PropelApi.configuration.respond_to?(:version)
167
+ config_version = PropelApi.configuration.version
168
+ return config_version if config_version.present?
169
+ end
170
+ rescue => e
171
+ # Configuration not available, continue to default
172
+ end
173
+
174
+ # Default fallback
175
+ 'v1'
176
+ end
177
+
178
+ # Display helpers for logging
179
+ def namespace_display
180
+ @api_namespace.present? ? @api_namespace : 'none'
181
+ end
182
+
183
+ def version_display
184
+ @api_version.present? ? @api_version : 'none'
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,457 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'configuration_methods'
4
+ require_relative 'path_generation_methods'
5
+
6
+ ##
7
+ # Named base class for PropelApi generators that work with named resources
8
+ # Inherits name handling from Rails::Generators::NamedBase and adds PropelApi functionality
9
+ #
10
+ module PropelApi
11
+ class NamedBase < Rails::Generators::NamedBase
12
+ include PropelApi::ConfigurationMethods
13
+ include PropelApi::PathGenerationMethods
14
+
15
+ protected
16
+
17
+ def model_exists?
18
+ File.exist?(File.join(destination_root, "app/models/#{file_name}.rb"))
19
+ end
20
+
21
+ # Controller generation helper methods
22
+ def controller_file_name
23
+ "#{controller_name.underscore}_controller"
24
+ end
25
+
26
+ def controller_name
27
+ "#{class_name.pluralize}"
28
+ end
29
+
30
+ def controller_class_name_with_namespace
31
+ class_parts = []
32
+ class_parts << @api_namespace.camelize if @api_namespace.present?
33
+ class_parts << @api_version.upcase if @api_version.present?
34
+ class_parts << controller_name
35
+ class_parts.join('::')
36
+ end
37
+
38
+ def controller_class_name
39
+ "#{class_name.pluralize}"
40
+ end
41
+
42
+ # Route helper methods
43
+ def route_name
44
+ file_name.pluralize
45
+ end
46
+
47
+ def api_route_path
48
+ path_parts = []
49
+ path_parts << @api_namespace if @api_namespace.present?
50
+ path_parts << @api_version if @api_version.present?
51
+ path_parts << route_name
52
+ '/' + path_parts.join('/')
53
+ end
54
+
55
+ def api_route_helper
56
+ path_parts = []
57
+ path_parts << @api_namespace if @api_namespace.present?
58
+ path_parts << @api_version if @api_version.present?
59
+ path_parts << route_name
60
+ path_parts.join("_")
61
+ end
62
+
63
+ # Model introspection methods for existing models
64
+ def introspect_model_attributes
65
+ return [] unless model_exists?
66
+
67
+ model_file_path = File.join(destination_root, "app/models/#{file_name}.rb")
68
+ model_content = File.read(model_file_path)
69
+
70
+ # Extract basic attribute information from model file
71
+ attributes = []
72
+
73
+ # Look for belongs_to associations
74
+ model_content.scan(/belongs_to\s+:(\w+)(?:,\s*(.*))?/) do |association, options|
75
+ # Convert belongs_to to reference attribute
76
+ attr_name = "#{association}_id"
77
+ unless should_exclude_from_permitted_params?(attr_name, :integer)
78
+ attributes << {
79
+ name: attr_name,
80
+ type: :integer,
81
+ reference: association
82
+ }
83
+ end
84
+ end
85
+
86
+ # Look for basic validations that might indicate attributes
87
+ model_content.scan(/validates\s+:(\w+)/) do |attr|
88
+ attr_name = attr.first
89
+ unless attributes.any? { |a| a[:name] == attr_name } || should_exclude_from_permitted_params?(attr_name, :string)
90
+ attributes << {
91
+ name: attr_name,
92
+ type: :string # Default to string for validated attributes
93
+ }
94
+ end
95
+ end
96
+
97
+ # Try to introspect from schema if available
98
+ if defined?(ActiveRecord::Base) && model_class_defined?
99
+ begin
100
+ model_class = class_name.constantize
101
+ if model_class.table_exists?
102
+ model_class.columns.each do |column|
103
+ attr_name = column.name
104
+ attr_type = schema_type_to_generator_type(column.type)
105
+
106
+ # Skip if we already have this attribute or if it should be excluded
107
+ next if attributes.any? { |a| a[:name] == attr_name } || should_exclude_from_permitted_params?(attr_name, attr_type)
108
+
109
+ attributes << {
110
+ name: attr_name,
111
+ type: attr_type
112
+ }
113
+ end
114
+ end
115
+ rescue => e
116
+ # Model class not available or database not connected
117
+ # Fall back to file-based introspection
118
+ end
119
+ end
120
+
121
+ attributes
122
+ end
123
+
124
+ def introspected_permitted_params
125
+ return [] unless model_exists?
126
+
127
+ attributes = introspect_model_attributes
128
+
129
+ # Convert attributes to permitted param names (filtering already applied in introspect_model_attributes)
130
+ params = attributes.map do |attr|
131
+ attr[:name]
132
+ end
133
+
134
+ # Always include common multi-tenancy params if not already included
135
+ %w[organization_id agency_id].each do |param|
136
+ params << param unless params.include?(param)
137
+ end
138
+
139
+ params
140
+ end
141
+
142
+ def validate_attributes_exist
143
+ return if options[:all_attributes] || !model_exists?
144
+
145
+ # If attributes were specified but model exists, validate they exist
146
+ return if attributes.empty?
147
+
148
+ model_attributes = introspect_model_attributes
149
+ model_attr_names = model_attributes.map { |attr| attr[:name] }
150
+
151
+ # Check if any specified attributes don't exist in the model
152
+ missing_attributes = attributes.map(&:name) - model_attr_names
153
+
154
+ if missing_attributes.any?
155
+ say "Warning: The following attributes don't exist in #{class_name}:", :yellow
156
+ missing_attributes.each { |attr| say " - #{attr}", :yellow }
157
+ say "Use --all-attributes to auto-detect model attributes", :cyan
158
+ end
159
+ end
160
+
161
+ # Shared controller and route generation methods
162
+ def create_propel_controller
163
+ initialize_propel_api_settings
164
+
165
+ if behavior == :revoke
166
+ controller_file_path = "app/controllers/#{controller_path}/#{controller_file_name}.rb"
167
+ remove_file controller_file_path
168
+ say "Removed controller: #{controller_file_path}", :red
169
+ else
170
+ case @adapter
171
+ when 'propel_facets'
172
+ template "scaffold/facet_controller_template.rb.tt", "app/controllers/#{controller_path}/#{controller_file_name}.rb"
173
+ when 'graphiti'
174
+ template "scaffold/graphiti_controller_template.rb.tt", "app/controllers/#{controller_path}/#{controller_file_name}.rb"
175
+
176
+ # Generate Graphiti resource if this generator supports it
177
+ if should_generate_graphiti_resource?
178
+ template "scaffold/graphiti_resource_template.rb.tt", "app/resources/#{file_name}_resource.rb"
179
+ end
180
+ else
181
+ raise "Unknown adapter: #{@adapter}. Run 'rails generate propel_api:install' first."
182
+ end
183
+ end
184
+ end
185
+
186
+ def create_propel_routes
187
+ if behavior == :revoke
188
+ remove_routes
189
+ else
190
+ insert_routes
191
+ end
192
+ end
193
+
194
+ def create_propel_tests(test_types: [])
195
+ if behavior == :revoke
196
+ # Remove test files
197
+ test_types.each do |test_type|
198
+ case test_type
199
+ when :model
200
+ remove_file "test/models/#{file_name}_test.rb"
201
+ when :controller
202
+ remove_file "test/controllers/#{controller_path}/#{controller_file_name}_test.rb"
203
+ when :integration
204
+ remove_file "test/integration/#{file_name}_api_test.rb"
205
+ when :fixtures
206
+ remove_file "test/fixtures/#{table_name}.yml"
207
+ end
208
+ end
209
+ else
210
+ # Generate test files
211
+ test_types.each do |test_type|
212
+ case test_type
213
+ when :model
214
+ template "tests/model_test_template.rb.tt", "test/models/#{file_name}_test.rb"
215
+ when :controller
216
+ template "tests/controller_test_template.rb.tt", "test/controllers/#{controller_path}/#{controller_file_name}_test.rb"
217
+ when :integration
218
+ template "tests/integration_test_template.rb.tt", "test/integration/#{file_name}_api_test.rb"
219
+ when :fixtures
220
+ template "tests/fixtures_template.yml.tt", "test/fixtures/#{table_name}.yml"
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ def show_propel_completion_message(resource_type:, generated_files:, next_steps:)
227
+ if behavior == :revoke
228
+ say "\n" + "="*70, :red
229
+ say "#{resource_type} Destroyed Successfully!", :red
230
+ say "="*70, :red
231
+ say "\n🗑️ Removed files:", :red
232
+ generated_files.each { |file| say " #{file}", :red }
233
+ say "\n💡 Don't forget to:", :blue
234
+ say " • Remove any manual customizations", :blue
235
+ say " • Check for any remaining references", :blue
236
+ say "="*70, :red
237
+ else
238
+ say "\n" + "="*70, :green
239
+ say "#{resource_type} Generated Successfully!", :green
240
+ say "="*70, :green
241
+ say "\n📦 Generated files:", :green
242
+ generated_files.each { |file| say " #{file}", :green }
243
+ say "\n🔧 Configuration:", :cyan
244
+ say " • Adapter: #{@adapter}", :cyan
245
+ say " • Namespace: #{namespace_display}", :cyan
246
+ say " • Version: #{version_display}", :cyan
247
+ say "\n🚀 Next steps:", :blue
248
+ next_steps.each { |step| say " • #{step}", :blue }
249
+ say "="*70, :green
250
+ end
251
+ end
252
+
253
+ private
254
+
255
+ def should_exclude_from_permitted_params?(attribute_name, attribute_type)
256
+ # Try to use the centralized configurable filter if available
257
+ begin
258
+ if defined?(PropelApi) && PropelApi.respond_to?(:attribute_filter)
259
+ return PropelApi.attribute_filter.exclude_from_permitted_params?(attribute_name, attribute_type)
260
+ end
261
+ rescue => e
262
+ # Configuration not available, fall back to default patterns
263
+ end
264
+
265
+ # Fallback: Use the same patterns as templates for consistency
266
+ attr_str = attribute_name.to_s
267
+
268
+ # Exclude timestamps and internal fields (same as templates)
269
+ excluded_patterns = /\A(created_at|updated_at|deleted_at|password_digest|reset_password_token|confirmation_token|unlock_token)\z/i
270
+
271
+ # Always exclude security-sensitive fields (same as templates)
272
+ security_patterns = /(password|digest|token|secret|key|salt|encrypted|confirmation|unlock|reset|api_key|access_token|refresh_token)/i
273
+
274
+ # Exclude binary and large data types (same as templates)
275
+ excluded_types = [:binary]
276
+
277
+ # Apply the same filtering logic as templates
278
+ excluded_patterns.match?(attr_str) ||
279
+ security_patterns.match?(attr_str) ||
280
+ excluded_types.include?(attribute_type)
281
+ end
282
+
283
+ def model_class_defined?
284
+ begin
285
+ class_name.constantize
286
+ true
287
+ rescue NameError
288
+ false
289
+ end
290
+ end
291
+
292
+ def schema_type_to_generator_type(schema_type)
293
+ case schema_type
294
+ when :integer
295
+ :integer
296
+ when :decimal, :float
297
+ :decimal
298
+ when :boolean
299
+ :boolean
300
+ when :text
301
+ :text
302
+ when :datetime, :timestamp
303
+ :datetime
304
+ when :date
305
+ :date
306
+ when :time
307
+ :time
308
+ else
309
+ :string
310
+ end
311
+ end
312
+
313
+ # Subclasses can override this to control Graphiti resource generation
314
+ def should_generate_graphiti_resource?
315
+ false
316
+ end
317
+
318
+ # Route management helper methods
319
+ def insert_routes
320
+ routes_file = File.join(destination_root, "config/routes.rb")
321
+ return unless File.exist?(routes_file)
322
+
323
+ routes_content = File.read(routes_file)
324
+
325
+ if @api_namespace.present? && @api_version.present?
326
+ # Both namespace and version: insert into existing or create new structure
327
+ if has_nested_namespace?(routes_content, @api_namespace, @api_version)
328
+ resource_line = " resources :#{route_name}"
329
+ insert_into_nested_namespace(routes_content, @api_namespace, @api_version, resource_line)
330
+ else
331
+ # Create new nested namespace structure
332
+ # Rails route method adds 2 spaces to each line, so we need to adjust for that
333
+ route_content = "namespace :#{@api_namespace} do\n namespace :#{@api_version} do\n resources :#{route_name}\n end\nend"
334
+ route route_content
335
+ end
336
+ elsif @api_namespace.present?
337
+ # Only namespace: insert into existing or create new
338
+ if has_single_namespace?(routes_content, @api_namespace)
339
+ resource_line = " resources :#{route_name}"
340
+ insert_into_single_namespace(routes_content, @api_namespace, resource_line)
341
+ else
342
+ # Create new namespace
343
+ # Rails route method adds 2 spaces to each line, so we need to adjust for that
344
+ route_content = "namespace :#{@api_namespace} do\n resources :#{route_name}\nend"
345
+ route route_content
346
+ end
347
+ else
348
+ # No namespace: simple resources
349
+ route "resources :#{route_name}"
350
+ end
351
+ end
352
+
353
+ def remove_routes
354
+ routes_file = File.join(destination_root, "config/routes.rb")
355
+ return unless File.exist?(routes_file)
356
+
357
+ routes_content = File.read(routes_file)
358
+
359
+ # Try to remove the resources line specifically
360
+ resource_line_patterns = [
361
+ /^\s*resources :#{route_name}\s*$/m,
362
+ /^\s*resources :#{route_name},.*$/m,
363
+ /\s*resources :#{route_name}\s*\n/m
364
+ ]
365
+
366
+ updated_content = routes_content
367
+ removed = false
368
+
369
+ resource_line_patterns.each do |pattern|
370
+ if updated_content.match?(pattern)
371
+ updated_content = updated_content.gsub(pattern, '')
372
+ removed = true
373
+ break
374
+ end
375
+ end
376
+
377
+ if removed
378
+ # Clean up empty namespace blocks after removing resources
379
+ updated_content = cleanup_empty_namespaces(updated_content)
380
+
381
+ # Clean up extra newlines
382
+ updated_content = updated_content.gsub(/\n\n+/, "\n\n")
383
+
384
+ File.write(routes_file, updated_content)
385
+ say "Removed resources :#{route_name} from routes", :red
386
+ else
387
+ say "Could not find routes for #{route_name} to remove. Please check config/routes.rb manually", :yellow
388
+ end
389
+ end
390
+
391
+ def has_nested_namespace?(content, namespace, version)
392
+ content.match?(/namespace\s+:#{namespace}\s+do.*?namespace\s+:#{version}\s+do/m)
393
+ end
394
+
395
+ def has_single_namespace?(content, namespace)
396
+ content.match?(/namespace\s+:#{namespace}\s+do/)
397
+ end
398
+
399
+ def insert_into_nested_namespace(content, namespace, version, resource_line)
400
+ # Check if resource already exists
401
+ existing_pattern = /namespace\s+:#{namespace}\s+do.*?namespace\s+:#{version}\s+do.*?resources\s+:#{route_name}/m
402
+
403
+ if content.match?(existing_pattern)
404
+ say "Route for #{route_name} already exists in #{namespace}/#{version} namespace", :yellow
405
+ return
406
+ end
407
+
408
+ # Find the position to insert the resource line
409
+ # Look for the end of the nested namespace's opening line
410
+ after_pattern = /namespace\s+:#{namespace}\s+do\s*\n\s*namespace\s+:#{version}\s+do\s*\n/
411
+
412
+ if content.match?(after_pattern)
413
+ # Insert the resource line with proper indentation (4 spaces for nested namespace)
414
+ insert_into_file "config/routes.rb", "#{resource_line}\n", :after => after_pattern
415
+ else
416
+ # This shouldn't happen if has_nested_namespace? returned true, but handle it
417
+ route_content = "namespace :#{namespace} do\n namespace :#{version} do\n#{resource_line}\n end\nend"
418
+ route route_content
419
+ end
420
+ end
421
+
422
+ def insert_into_single_namespace(content, namespace, resource_line)
423
+ # Check if resource already exists
424
+ existing_pattern = /namespace\s+:#{namespace}\s+do.*?resources\s+:#{route_name}/m
425
+
426
+ if content.match?(existing_pattern)
427
+ say "Route for #{route_name} already exists in #{namespace} namespace", :yellow
428
+ return
429
+ end
430
+
431
+ # Find the position to insert the resource line
432
+ # Look for the end of the namespace's opening line
433
+ after_pattern = /namespace\s+:#{namespace}\s+do\s*\n/
434
+
435
+ if content.match?(after_pattern)
436
+ # Insert the resource line with proper indentation (resource_line already has correct 2 spaces)
437
+ insert_into_file "config/routes.rb", "#{resource_line}\n", :after => after_pattern
438
+ else
439
+ # This shouldn't happen if has_single_namespace? returned true, but handle it
440
+ route_content = "namespace :#{namespace} do\n resources :#{route_name}\n end"
441
+ route route_content
442
+ end
443
+ end
444
+
445
+ def cleanup_empty_namespaces(content)
446
+ # Remove empty nested namespaces (namespace with only whitespace)
447
+ # This handles: namespace :api do\n namespace :v1 do\n\n end\nend
448
+ content = content.gsub(/namespace\s+:\w+\s+do\s*\n\s*namespace\s+:\w+\s+do\s*\n\s*end\s*\n\s*end/m, '')
449
+
450
+ # Remove empty single namespaces
451
+ # This handles: namespace :api do\n\nend
452
+ content = content.gsub(/namespace\s+:\w+\s+do\s*\n\s*end/m, '')
453
+
454
+ content
455
+ end
456
+ end
457
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Module providing API path generation methods for PropelApi generators
5
+ #
6
+ module PropelApi
7
+ module PathGenerationMethods
8
+
9
+ protected
10
+
11
+ # Generate the full API controller class name with namespace and version
12
+ def api_controller_class_name
13
+ class_parts = []
14
+ class_parts << @api_namespace.camelize if @api_namespace.present?
15
+ class_parts << @api_version.upcase if @api_version.present?
16
+ class_parts << 'ApiController'
17
+ class_parts.join('::')
18
+ end
19
+
20
+ # Generate controller path for file creation
21
+ def controller_path
22
+ path_parts = []
23
+ path_parts << @api_namespace if @api_namespace.present?
24
+ path_parts << @api_version if @api_version.present?
25
+ path_parts.join('/')
26
+ end
27
+
28
+ # Generate API route path prefix
29
+ def api_route_prefix
30
+ path_parts = []
31
+ path_parts << @api_namespace if @api_namespace.present?
32
+ path_parts << @api_version if @api_version.present?
33
+ '/' + path_parts.join('/')
34
+ end
35
+
36
+ # Generate API controller file path
37
+ def api_controller_path
38
+ path_parts = ['app', 'controllers']
39
+ path_parts << @api_namespace if @api_namespace.present?
40
+ path_parts << @api_version if @api_version.present?
41
+ path_parts << 'api_controller.rb'
42
+ path_parts.join('/')
43
+ end
44
+ end
45
+ end