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,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Infers ActiveRecord relationships based on PropelAPI conventions
4
+ #
5
+ # Conventions:
6
+ # - All references default to belongs_to with has_many inverse
7
+ # - All relationships use dependent: :destroy by default
8
+ # - Class names match reference names (no STI support)
9
+ # - Polymorphic relationships use _parent suffix (resource_parent or resourceable_parent)
10
+ # - Through relationships create join tables
11
+ #
12
+ class RelationshipInferrer
13
+ attr_reader :model_name, :attributes, :options
14
+
15
+ def initialize(model_name, attributes, options = {})
16
+ @model_name = model_name
17
+ @attributes = attributes
18
+ @options = options
19
+ @inverse_relationships = {}
20
+ end
21
+
22
+ # Generate relationships for this model
23
+ def belongs_to_relationships
24
+ relationships = []
25
+
26
+ reference_attributes.each do |attribute|
27
+ if polymorphic_reference?(attribute)
28
+ relationships << polymorphic_belongs_to(attribute)
29
+ else
30
+ relationships << standard_belongs_to(attribute)
31
+ end
32
+ end
33
+
34
+ relationships.compact
35
+ end
36
+
37
+ # Generate inverse relationships that need to be added to other models
38
+ def inverse_relationships
39
+ @inverse_relationships
40
+ end
41
+
42
+ # Check if we should generate associations (can be disabled)
43
+ def should_generate_associations?
44
+ !options[:skip_associations]
45
+ end
46
+
47
+ private
48
+
49
+ def reference_attributes
50
+ attributes.select { |attr| attr.type == :references }
51
+ end
52
+
53
+ def polymorphic_reference?(attribute)
54
+ attribute.name.to_s.end_with?('_parent') ||
55
+ attribute.name.to_s.end_with?('able')
56
+ end
57
+
58
+
59
+
60
+ def standard_belongs_to(attribute)
61
+ reference_name = attribute.name.to_s
62
+ class_name = reference_name.camelize
63
+
64
+ # Add inverse relationship
65
+ add_inverse_relationship(class_name, standard_has_many)
66
+
67
+ # All references except organization are optional by default (PropelAPI convention)
68
+ if reference_name == 'organization'
69
+ "belongs_to :#{reference_name}"
70
+ else
71
+ "belongs_to :#{reference_name}, optional: true"
72
+ end
73
+ end
74
+
75
+ def polymorphic_belongs_to(attribute)
76
+ # Extract the base name for polymorphic association
77
+ if attribute.name.to_s.end_with?('_parent')
78
+ # resource_parent -> resource
79
+ base_name = attribute.name.to_s.gsub(/_parent$/, '')
80
+ elsif attribute.name.to_s.end_with?('able')
81
+ # commentable -> commentable
82
+ base_name = attribute.name.to_s
83
+ else
84
+ base_name = attribute.name.to_s
85
+ end
86
+
87
+ # Polymorphic associations are optional by default (PropelAPI convention)
88
+ "belongs_to :#{base_name}, polymorphic: true, optional: true"
89
+ end
90
+
91
+
92
+
93
+ def standard_has_many
94
+ "has_many :#{table_name}, dependent: :destroy"
95
+ end
96
+
97
+
98
+
99
+
100
+
101
+ def add_inverse_relationship(class_name, relationship)
102
+ @inverse_relationships[class_name] ||= []
103
+ @inverse_relationships[class_name] << relationship
104
+ end
105
+
106
+ def table_name
107
+ model_name.tableize
108
+ end
109
+
110
+ def singular_table_name
111
+ model_name.underscore
112
+ end
113
+
114
+ def class_name
115
+ model_name.camelize
116
+ end
117
+ end
@@ -0,0 +1,343 @@
1
+ require_relative '../core/base'
2
+ require_relative '../unpack/unpack_generator'
3
+
4
+ ##
5
+ # PropelApi installer that provides API generators for Rails applications
6
+ #
7
+ # This creates a COMPLETELY STANDALONE API generation system with no gem dependencies.
8
+ # Usage:
9
+ # rails generate propel_api:install # Full standalone system (runtime + generators)
10
+ #
11
+ # This generator:
12
+ # 1. Installs API controller configuration and examples
13
+ # 2. Automatically extracts generator logic to lib/generators/propel_api/
14
+ # 3. Creates a system that requires NO gem dependencies for generation
15
+ #
16
+ # After installation, you can remove 'propel_api' from your Gemfile completely.
17
+ # All code is in your application and fully customizable.
18
+ #
19
+ module PropelApi
20
+ class InstallGenerator < PropelApi::Base
21
+ source_root File.expand_path("../templates", __dir__)
22
+
23
+ desc "Install API controllers with configurable serialization backend"
24
+
25
+ def copy_api_controller
26
+ initialize_propel_api_settings
27
+
28
+ if behavior == :revoke
29
+ say "Removing API controller with #{@adapter} adapter", :red
30
+ say "Using namespace: #{namespace_display} version: #{version_display}", :blue
31
+ else
32
+ say "Installing API controller with #{@adapter} adapter", :green
33
+ say "Using namespace: #{namespace_display} version: #{version_display}", :blue
34
+ end
35
+
36
+ case @adapter
37
+ when 'graphiti'
38
+ copy_graphiti_controller
39
+ generate_graphiti_resources if respond_to?(:generate_graphiti_resources, true)
40
+ when 'propel_facets'
41
+ copy_propel_facets_controller
42
+ else
43
+ raise "Unknown adapter: #{@adapter}. Supported: 'propel_facets', 'graphiti'"
44
+ end
45
+ end
46
+
47
+ def create_configuration
48
+ if behavior == :revoke
49
+ remove_file "config/initializers/propel_api.rb"
50
+ # Rails automatically handles file removal, just show appropriate message
51
+ say "Removed PropelApi configuration", :red
52
+ else
53
+ # Create the PropelApi configuration initializer
54
+ template "config/propel_api.rb.tt", "config/initializers/propel_api.rb"
55
+ say "Created PropelApi configuration with your selected options", :green
56
+ end
57
+ end
58
+
59
+ def create_api_routes
60
+ if behavior == :revoke
61
+ # Remove API namespace structure
62
+ remove_api_namespace
63
+ else
64
+ # Add basic API namespace structure (defaults to api/v1)
65
+ add_api_namespace
66
+ end
67
+ end
68
+
69
+ def add_required_gems
70
+ if behavior == :revoke
71
+ say "💎 Note: Gems added during installation are still in your Gemfile", :yellow
72
+ say " Remove manually if no longer needed: faker, pagy, has_scope, ransack#{', graphiti, graphiti-rails, vandal_ui' if @adapter == 'graphiti'}", :yellow
73
+ else
74
+ case @adapter
75
+ when 'graphiti'
76
+ add_graphiti_gems
77
+ when 'propel_facets'
78
+ add_propel_facets_gems
79
+ end
80
+ end
81
+ end
82
+
83
+ def show_setup_instructions
84
+ say_setup_instructions
85
+ end
86
+
87
+ def extract_generator_for_customization
88
+ generator_path = "lib/generators/propel_api"
89
+
90
+ if File.exist?(generator_path)
91
+ say ""
92
+ say "📦 Generator logic already extracted at #{generator_path}/", :blue
93
+ say "💡 Skipping extraction to preserve your customizations", :cyan
94
+ else
95
+ say ""
96
+ say "📦 Extracting generator logic for full customization...", :blue
97
+
98
+ # Automatically run the unpack generator to extract generator logic
99
+ invoke PropelApi::UnpackGenerator, [], { force: false }
100
+
101
+ say ""
102
+ say "✅ Generator logic extracted to lib/generators/propel_api/", :green
103
+ say "💡 Your application is now completely standalone - no gem dependency needed for generators!", :cyan
104
+ say "🗑️ You can now remove 'propel_api' from your Gemfile", :yellow
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ def copy_propel_facets_controller
111
+ template "controllers/api_controller_propel_facets.rb", api_controller_path
112
+ copy_example_controller
113
+
114
+ # Copy propel_facets specific dependencies
115
+ copy_propel_facets_concerns if respond_to?(:copy_propel_facets_concerns, true)
116
+ end
117
+
118
+ def copy_graphiti_controller
119
+ template "controllers/api_controller_graphiti.rb", api_controller_path
120
+ copy_example_controller
121
+
122
+ # Graphiti doesn't need additional concerns - it's self-contained
123
+ say "Remember to create Graphiti resources for your models:", :cyan
124
+ say " rails generate graphiti:resource YourModel", :yellow
125
+ end
126
+
127
+ def copy_example_controller
128
+ if behavior == :revoke
129
+ # Rails automatically handles file removal, just show appropriate message
130
+ say "📝 Removed example controller at doc/api_controller_example.rb", :red
131
+ else
132
+ # Create example controller showing usage patterns
133
+ @engine = @adapter
134
+ @resource_name = "Post" # Use a generic example name
135
+
136
+ template "controllers/example_controller.rb.tt", "doc/api_controller_example.rb"
137
+ say "📝 Created example controller at doc/api_controller_example.rb", :blue
138
+ end
139
+ end
140
+
141
+ def add_propel_facets_gems
142
+ add_gem_if_missing 'faker', '~> 3.5', 'Realistic seed data generation'
143
+ add_gem_if_missing 'pagy', '~> 9.0', 'Pagination for Propel Facets API'
144
+ add_gem_if_missing 'has_scope', '~> 0.8.0', 'Scope filtering for Propel Facets API'
145
+ add_gem_if_missing 'ransack', '~> 4.0', 'Advanced search for Propel Facets API'
146
+
147
+ say "Don't forget to run: bundle install", :yellow
148
+ end
149
+
150
+ def add_graphiti_gems
151
+ add_gem_if_missing 'faker', '~> 3.5', 'Realistic seed data generation'
152
+ add_gem_if_missing 'graphiti', '~> 1.4', 'JSON:API resource framework'
153
+ add_gem_if_missing 'graphiti-rails', '~> 0.2', 'Rails integration for Graphiti'
154
+ add_gem_if_missing 'vandal_ui', '~> 0.4', 'Interactive API documentation for Graphiti'
155
+
156
+ say "Don't forget to run: bundle install", :yellow
157
+ end
158
+
159
+ def add_gem_if_missing(gem_name, version, comment)
160
+ gemfile_content = File.read(File.join(destination_root, 'Gemfile'))
161
+
162
+ # Check if gem is already present
163
+ if gemfile_content.match(/gem\s+['"]#{gem_name}['"]/)
164
+ say "#{gem_name} is already in Gemfile, skipping...", :blue
165
+ else
166
+ gem gem_name, version, comment: comment
167
+ say "Added #{gem_name} #{version} to Gemfile", :green
168
+ end
169
+ end
170
+
171
+ def say_setup_instructions
172
+ if behavior == :revoke
173
+ say "\n" + "="*60, :red
174
+ say "API Controller Removed Successfully!", :red
175
+ say "="*60, :red
176
+ say "\n🔧 PropelApi configuration removed from config/initializers/propel_api.rb", :cyan
177
+ say "📋 Removed: --adapter=#{@adapter} --namespace=#{@api_namespace || 'none'} --version=#{@api_version || 'none'}", :cyan
178
+ say "\n💡 To reinstall: rails generate propel_api:install", :blue
179
+ say "="*60, :red
180
+ else
181
+ say "\n" + "="*60, :green
182
+ say "API Controller Installed Successfully!", :green
183
+ say "="*60, :green
184
+
185
+ say "\n📦 System Status: Completely standalone - no gem dependency!", :green
186
+ say "💡 Optional: Remove 'propel_api' from Gemfile (system is fully extracted)", :cyan
187
+
188
+ say "\n🔧 Configuration saved to config/initializers/propel_api.rb", :cyan
189
+ say "📋 Your defaults: --adapter=#{@adapter} --namespace=#{@api_namespace || 'none'} --version=#{@api_version || 'none'}", :cyan
190
+
191
+ case @adapter
192
+ when 'propel_facets'
193
+ say_propel_facets_instructions
194
+ when 'graphiti'
195
+ say_graphiti_instructions
196
+ end
197
+
198
+ say "\n🎨 Customization:", :bold
199
+ say "• Generator logic: lib/generators/propel_api/propel_api_generator.rb", :blue
200
+ say "• Templates: lib/generators/propel_api/templates/", :blue
201
+ say "• Modify any part of the system - it's all yours now!", :cyan
202
+
203
+ say "="*60, :green
204
+ end
205
+ end
206
+
207
+ def say_propel_facets_instructions
208
+ say "\n📋 Propel Facets Setup Instructions:", :cyan
209
+ say "\n1. Install the propel_facets generator if not already done:"
210
+ say " rails generate propel_facets", :yellow
211
+ say "\n2. Your API controller uses the propel_facets serialization system"
212
+ say "3. Add facets to your models using:"
213
+ say " json_facet :summary, fields: [:title, :excerpt]", :yellow
214
+ say "\n4. Create controllers that inherit from #{api_controller_class_name}:"
215
+ say " class #{api_controller_class_name.gsub('ApiController', 'UsersController')} < #{api_controller_class_name}", :yellow
216
+ say " permitted_params :name, :email", :yellow
217
+ say " connect_facet :short, actions: [:index]", :yellow
218
+ say " connect_facet :details, actions: [:show]", :yellow
219
+ say " end", :yellow
220
+ say "\n5. See doc/api_controller_example.rb for complete usage examples"
221
+ say "6. Your API endpoints will be available at: #{api_route_prefix}/..."
222
+ say "7. Pagination and filtering are built-in via Pagy and HasScope"
223
+ say "\n8. Generate resources using:"
224
+ say " rails generate propel_api Resource field1:type field2:type", :yellow
225
+ end
226
+
227
+ def say_graphiti_instructions
228
+ say "\n🎯 Graphiti Setup Instructions:", :cyan
229
+ say "\n1. Create resource classes for your models:"
230
+ say " rails generate graphiti:resource User", :yellow
231
+ say "\n2. Create controllers that inherit from #{api_controller_class_name}:"
232
+ say " class #{api_controller_class_name.gsub('ApiController', 'UsersController')} < #{api_controller_class_name}", :yellow
233
+ say " self.resource = UserResource", :yellow
234
+ say " end", :yellow
235
+ say "\n3. Add to your application.rb or initializer:"
236
+ say " Graphiti.configure { |c| c.base_url = Rails.application.routes.url_helpers.root_url }", :yellow
237
+ say "\n4. See doc/api_controller_example.rb for complete usage examples"
238
+ say "5. Visit /vandal for interactive API documentation"
239
+ say "6. Your API follows JSON:API specification automatically"
240
+ say "7. Use the Spraypaint.js client for frontend integration"
241
+ say "8. API endpoints: #{api_route_prefix}/... with JSON:API format"
242
+ say "\n9. Generate resources using:"
243
+ say " rails generate propel_api Resource field1:type field2:type", :yellow
244
+ end
245
+
246
+ # Propel orchestration methods - only active when called by Propel installer
247
+ def propel_orchestrated_adapter
248
+ return nil unless called_by_propel_installer?
249
+
250
+ begin
251
+ if defined?(Propel) && Propel.configuration.respond_to?(:api_adapter)
252
+ return Propel.configuration.api_adapter
253
+ end
254
+ rescue => e
255
+ # Propel configuration not available
256
+ end
257
+
258
+ nil
259
+ end
260
+
261
+ def propel_orchestrated_namespace
262
+ return nil unless called_by_propel_installer?
263
+
264
+ begin
265
+ if defined?(Propel) && Propel.configuration.respond_to?(:api_namespace)
266
+ return Propel.configuration.api_namespace
267
+ end
268
+ rescue => e
269
+ # Propel configuration not available
270
+ end
271
+
272
+ nil
273
+ end
274
+
275
+ def propel_orchestrated_version
276
+ return nil unless called_by_propel_installer?
277
+
278
+ begin
279
+ if defined?(Propel) && Propel.configuration.respond_to?(:api_version)
280
+ return Propel.configuration.api_version
281
+ end
282
+ rescue => e
283
+ # Propel configuration not available
284
+ end
285
+
286
+ nil
287
+ end
288
+
289
+ def called_by_propel_installer?
290
+ # Check if we're being called by Propel installer by examining the call stack
291
+ caller.any? { |line| line.include?('propel/install_generator') }
292
+ end
293
+
294
+ def add_api_namespace
295
+ initialize_propel_api_settings
296
+
297
+ # Use defaults: api/v1 (these come from the PropelApi configuration)
298
+ namespace = @api_namespace || 'api'
299
+ version = @api_version || 'v1'
300
+
301
+ # Create the namespace structure (always use nested structure for consistency)
302
+ route_content = "namespace :#{namespace} do\n namespace :#{version} do\n # Add your API routes here\n # Example: resources :users\n end\n end"
303
+
304
+ # Check if namespace already exists
305
+ routes_file = File.join(destination_root, "config/routes.rb")
306
+ if File.exist?(routes_file)
307
+ routes_content = File.read(routes_file)
308
+
309
+ # Check if API namespace already exists
310
+ if routes_content.match?(/namespace\s+:#{namespace}/)
311
+ say "API namespace already exists in routes", :blue
312
+ return
313
+ end
314
+ end
315
+
316
+ route route_content
317
+ say "Added API namespace to routes (#{namespace}/#{version})", :green
318
+ end
319
+
320
+ def remove_api_namespace
321
+ initialize_propel_api_settings
322
+
323
+ namespace = @api_namespace || 'api'
324
+ version = @api_version || 'v1'
325
+
326
+ routes_file = File.join(destination_root, "config/routes.rb")
327
+ return unless File.exist?(routes_file)
328
+
329
+ routes_content = File.read(routes_file)
330
+
331
+ # Remove the nested namespace block (api/v1 structure)
332
+ pattern = /namespace\s+:#{namespace}\s+do.*?namespace\s+:#{version}\s+do.*?end.*?end/m
333
+
334
+ if routes_content.match?(pattern)
335
+ updated_content = routes_content.gsub(pattern, '')
336
+ # Clean up extra newlines
337
+ updated_content = updated_content.gsub(/\n\n+/, "\n\n")
338
+ File.write(routes_file, updated_content)
339
+ say "Removed API namespace from routes (#{namespace}/#{version})", :red
340
+ end
341
+ end
342
+ end
343
+ end