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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +59 -0
- data/LICENSE +21 -0
- data/README.md +320 -0
- data/Rakefile +36 -0
- data/lib/generators/propel_api/USAGE +8 -0
- data/lib/generators/propel_api/controller/controller_generator.rb +208 -0
- data/lib/generators/propel_api/core/base.rb +19 -0
- data/lib/generators/propel_api/core/configuration_methods.rb +187 -0
- data/lib/generators/propel_api/core/named_base.rb +457 -0
- data/lib/generators/propel_api/core/path_generation_methods.rb +45 -0
- data/lib/generators/propel_api/core/relationship_inferrer.rb +117 -0
- data/lib/generators/propel_api/install/install_generator.rb +343 -0
- data/lib/generators/propel_api/resource/resource_generator.rb +433 -0
- data/lib/generators/propel_api/templates/config/propel_api.rb.tt +149 -0
- data/lib/generators/propel_api/templates/controllers/api_controller_graphiti.rb +79 -0
- data/lib/generators/propel_api/templates/controllers/api_controller_propel_facets.rb +76 -0
- data/lib/generators/propel_api/templates/controllers/example_controller.rb.tt +96 -0
- data/lib/generators/propel_api/templates/scaffold/facet_controller_template.rb.tt +80 -0
- data/lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt +141 -0
- data/lib/generators/propel_api/templates/scaffold/graphiti_controller_template.rb.tt +82 -0
- data/lib/generators/propel_api/templates/scaffold/graphiti_model_template.rb.tt +32 -0
- data/lib/generators/propel_api/templates/seeds/seeds_template.rb.tt +493 -0
- data/lib/generators/propel_api/templates/tests/controller_test_template.rb.tt +485 -0
- data/lib/generators/propel_api/templates/tests/fixtures_template.yml.tt +250 -0
- data/lib/generators/propel_api/templates/tests/integration_test_template.rb.tt +487 -0
- data/lib/generators/propel_api/templates/tests/model_test_template.rb.tt +252 -0
- data/lib/generators/propel_api/unpack/unpack_generator.rb +304 -0
- data/lib/propel_api.rb +3 -0
- 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
|