crudable-rails 1.3 → 1.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55f8eea5d65ce4caa175212c791a16bf9513306f54924b859868f6e62dba4011
4
- data.tar.gz: 5fad24b510a2b29ab90b77e784b06cf114cb54fb11001dbb582bb2353ba02ac1
3
+ metadata.gz: 6eb33d21180100aca344bcdc68bb1c170e28bff416b95c3bb4332fcb7c1eef16
4
+ data.tar.gz: a08037ec9a25220ba87697becdc928bf78400240088fb1f4c5157730dbfae162
5
5
  SHA512:
6
- metadata.gz: 4e0242ef6e4a90c2b83f5cfdfd535776f7ec233c227f88d1e6ec2d39d6d9bd766ae8e650b452078d72c30327d3de94a2ac965f3dfa0d988e53a4fc2be6add0d2
7
- data.tar.gz: 4af78bf8f8ffe06a7ca77f46b34c746db1d137ad53788af62434347db7d7a4769c7b0f4577d9087a368ef215145c92936087d2b26dd96f7912e74467aa5a7479
6
+ metadata.gz: ec33c0349fa255e003542bd431b5a84ea4b49ca981713409f372655944158dc9656070499d67c915d6dfb70e635136f736310ce84522bfa4e268b94bfb879896
7
+ data.tar.gz: 2226bedef0b794653978bfd8deab2302d2023909857de65bbec4e600df3a545a171a2c9575271a3a8ca1e6041f3fdec9b86fc1775dcf2dc6d65fd543057ccf84
data/README.md CHANGED
@@ -34,17 +34,7 @@ $ gem install crudable-rails
34
34
 
35
35
  ### Controller Setup
36
36
 
37
- To use crudable-rails in your controllers, call the `crudable` method. You can also specify if the controller is nested by passing the `nested: true` option.
38
-
39
- ```ruby
40
- class ProductsController < ApplicationController
41
- crudable
42
- end
43
-
44
- class ProductSizesController < ApplicationController
45
- crudable nested: true
46
- end
47
- ```
37
+ To use crudable-rails in your controllers, call the `crudable` method.
48
38
 
49
39
  ## Customizing CRUD Actions
50
40
 
@@ -137,6 +127,14 @@ This method should return a boolean value to determine if the resource should be
137
127
 
138
128
  This method should return a boolean value to determine if the resource is a singleton. Default: `false`.
139
129
 
130
+ ### `use_parent_as_scope?`
131
+
132
+ This method should return a boolean value to determine if the resource should be found using the nested parent. Default: `true`.
133
+
134
+ ### `parent_id_param`
135
+
136
+ When using nested routes, this determines which `*_id` param is treated as the parent id. If multiple `*_id` params are present (multi-level nesting), Crudable will default to the **deepest/last** `*_id` from the route path parameters.
137
+
140
138
  ### `finder_param`
141
139
 
142
140
  This method should return the parameter used to find the resource. Default: `:id`.
@@ -147,12 +145,38 @@ This method should return a boolean value to determine if the resource should be
147
145
 
148
146
  ### `friendly_finders?`
149
147
 
150
- This method should return a boolean value to determine if the resource should be found using friendly finders. Default: `true` if FriendlyId is available, otherwise `false`.
148
+ This method should return a boolean value to determine if the resource should be found using friendly finders.
149
+
150
+ Default: `true` when `FriendlyId` is available **and** the model responds to `.friendly`, otherwise `false`.
151
+
152
+ ### `parent_friendly_finders?`
153
+
154
+ When using nested routes (e.g. `/:parent_id/:id`), this method controls whether the **parent** should be found using FriendlyId.
155
+
156
+ Default: `friendly_finders?` (and additionally requires `FriendlyId` to be defined and the parent model to respond to `.friendly`).
151
157
 
152
158
  ### `skip_initialize_create?`
153
159
 
154
160
  This method should return a boolean value to determine if the resource should be initialized on create. Default: `false`.
155
161
 
162
+ ### `authorize_with_pundit?`
163
+
164
+ This method controls whether Pundit authorization should be performed. By default, it returns `true` when Pundit is defined. You can override this method to customize authorization behavior based on feature flags, user roles, or other conditions.
165
+
166
+ For example:
167
+
168
+ ```ruby
169
+ class ProductsController < ApplicationController
170
+ crudable
171
+
172
+ private
173
+
174
+ def authorize_with_pundit?
175
+ super && current_user.present? && feature_enabled?(:authorization)
176
+ end
177
+ end
178
+ ```
179
+
156
180
  ### `(create|update)_params`
157
181
 
158
182
  These methods should return the permitted parameters for the resource as an array. They are optional and can be defined if create and update methods need different parameters allowed.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
- require "bundler/setup"
1
+ # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
3
+ require 'bundler/setup'
4
+
5
+ require 'bundler/gem_tasks'
@@ -5,47 +5,76 @@ module Crudable
5
5
  module Base
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ # Status code for unprocessable entity (422)
9
+ # Rails 7.1+ uses :unprocessable_content (replaces deprecated :unprocessable_entity)
10
+ UNPROCESSABLE_STATUS = :unprocessable_content
11
+
8
12
  included do
9
13
  include Crudable::Rails::Resourceable
14
+ include Crudable::Rails::Nestable
10
15
 
11
16
  before_action :find_resource, only: %i[show edit update destroy]
12
- before_action :authorize_class_action, only: %i[index] if defined?(Pundit)
13
- after_action :authorize_resource, only: %i[new show edit] if defined?(Pundit)
14
17
 
15
18
  decorates_assigned plural_resource_var_name.to_sym, resource_var_name.to_sym if defined?(Draper)
16
19
  end
17
20
 
18
21
  def index
22
+ authorize_class_action if authorize_with_pundit?
19
23
  instance_variable_set("@#{plural_resource_var_name}", resource_scope)
20
24
  paginate_resource
21
25
  end
22
26
 
23
- def show; end
27
+ def show
28
+ render_not_found if instance_variable_get("@#{resource_var_name}").blank?
29
+ authorize_resource if authorize_with_pundit?
30
+ end
24
31
 
25
32
  def new
26
- instance_variable_set("@#{resource_var_name}", resource_class.new)
33
+ instance = new_instance
34
+ # Ensure we always set a valid instance to prevent nil model warnings in Rails 8
35
+ raise ActiveRecord::RecordNotFound, "Could not create new #{resource_class.name}" if instance.nil?
36
+
37
+ instance_variable_set("@#{resource_var_name}", instance)
38
+ authorize_resource if authorize_with_pundit?
27
39
  end
28
40
 
29
- def create # rubocop:disable Metrics/MethodLength
30
- instance_variable_set("@#{resource_var_name}", resource_class.new(create_params)) unless skip_initialize_create?
31
- if defined?(Pundit)
41
+ def create
42
+ unless skip_initialize_create?
43
+ instance_variable_set("@#{resource_var_name}", new_instance)
44
+ instance_variable_get("@#{resource_var_name}").assign_attributes(create_params)
45
+ end
46
+ resource = instance_variable_get("@#{resource_var_name}")
47
+ # If skip_initialize_create? is true, resource might be nil - that's expected
48
+ # Only return 404 if we tried to initialize but resource is still nil
49
+ return render_not_found if resource.nil? && !skip_initialize_create?
50
+
51
+ # Only authorize if resource exists (skip authorization when skip_initialize_create? is true)
52
+ if authorize_with_pundit? && resource.present?
32
53
  before_authorize_create
33
54
  authorize_resource
34
55
  after_authorize_create
56
+ elsif authorize_with_pundit?
57
+ skip_authorization
35
58
  end
36
- if instance_variable_get("@#{resource_var_name}").save
59
+ if resource&.save
37
60
  on_successful_create
38
61
  on_successful_create_render
39
62
  else
40
63
  on_failed_create_setup
41
64
  on_failed_create_render
42
65
  end
66
+ rescue ActionController::ParameterMissing
67
+ head :bad_request
43
68
  end
44
69
 
45
- def edit; end
70
+ def edit
71
+ render_not_found if instance_variable_get("@#{resource_var_name}").blank?
72
+ authorize_resource if authorize_with_pundit?
73
+ end
46
74
 
47
75
  def update
48
- if defined?(Pundit)
76
+ render_not_found if instance_variable_get("@#{resource_var_name}").blank?
77
+ if authorize_with_pundit?
49
78
  before_authorize_update
50
79
  authorize_resource
51
80
  after_authorize_update
@@ -57,10 +86,12 @@ module Crudable
57
86
  on_failed_update_setup
58
87
  on_failed_update_render
59
88
  end
89
+ rescue ActionController::ParameterMissing
90
+ head :bad_request
60
91
  end
61
92
 
62
93
  def destroy
63
- authorize_resource(destroy_method) if defined?(Pundit)
94
+ authorize_resource(destroy_method) if authorize_with_pundit?
64
95
  if instance_variable_get("@#{resource_var_name}").send(destroy_method)
65
96
  on_successful_destroy
66
97
  on_successful_destroy_render
@@ -72,6 +103,10 @@ module Crudable
72
103
 
73
104
  private
74
105
 
106
+ def authorize_with_pundit?
107
+ defined?(Pundit)
108
+ end
109
+
75
110
  def destroy_method
76
111
  return :destroy unless defined?(Discard)
77
112
  return :destroy if params[:destroy]
@@ -85,11 +120,38 @@ module Crudable
85
120
 
86
121
  def find_resource
87
122
  resource = friendly_finders? ? resource_class.friendly : resource_class
88
- instance = singleton? ? resource.first : resource.find(params[finder_param])
123
+ instance = find_instance(resource)
89
124
 
90
125
  return redirect_to (resource_namespace + [instance]), status: :moved_permanently if should_redirect?(instance)
91
126
 
92
127
  instance_variable_set("@#{resource_var_name}", instance)
128
+ rescue ActiveRecord::RecordNotFound
129
+ render_not_found
130
+ end
131
+
132
+ def find_instance(resource)
133
+ if singleton?
134
+ resource.first
135
+ elsif parent_resource_association.present?
136
+ scope = instance_variable_get("@#{parent_var_name}").send(parent_resource_association)
137
+ scope = scope.friendly if friendly_finders?
138
+ scope.find(params[finder_param])
139
+ else
140
+ resource.find(params[finder_param])
141
+ end
142
+ end
143
+
144
+ def new_instance
145
+ if parent_resource_association.present?
146
+ parent = instance_variable_get("@#{parent_var_name}")
147
+ # If parent is nil, it means find_parent failed - this shouldn't happen
148
+ # but we guard against it to prevent nil errors
149
+ raise ActiveRecord::RecordNotFound, "Parent #{parent_var_name} not found" if parent.nil?
150
+
151
+ parent.send(parent_resource_association).new
152
+ else
153
+ resource_class.new
154
+ end
93
155
  end
94
156
 
95
157
  def should_redirect?(instance)
@@ -111,7 +173,27 @@ module Crudable
111
173
  end
112
174
 
113
175
  def authorizable_scope
114
- defined?(Pundit) ? policy_scope(resource_namespace + [resource_class]) : resource_class.all
176
+ scope = find_scope
177
+ return scope.all unless authorize_with_pundit?
178
+ # Check if Pundit::Authorization is available
179
+ return scope.all unless if respond_to?(:pundit_authorization_available?,
180
+ true)
181
+ pundit_authorization_available?
182
+ else
183
+ self.class.included_modules.include?(Pundit::Authorization) || respond_to?(
184
+ :policy_scope, true
185
+ )
186
+ end
187
+
188
+ policy_scope(resource_namespace + [scope])
189
+ end
190
+
191
+ def find_scope
192
+ if parent_resource_association.present?
193
+ instance_variable_get("@#{parent_var_name}").send(parent_resource_association)
194
+ else
195
+ resource_class
196
+ end
115
197
  end
116
198
 
117
199
  def paginate_resource?
@@ -140,6 +222,14 @@ module Crudable
140
222
  end
141
223
 
142
224
  def after_create_redirect_path
225
+ if respond_to?(:parent_present?) && parent_present? && instance_variable_get("@#{parent_var_name}")
226
+ parent = instance_variable_get("@#{parent_var_name}")
227
+ resource_namespace + [parent, plural_resource_var_name.to_sym]
228
+ else
229
+ resource_namespace + [plural_resource_var_name.to_sym]
230
+ end
231
+ rescue NoMethodError
232
+ # Fallback if parent_present? or parent_var_name methods aren't available
143
233
  resource_namespace + [plural_resource_var_name.to_sym]
144
234
  end
145
235
 
@@ -216,16 +306,24 @@ module Crudable
216
306
  def after_authorize_update; end
217
307
 
218
308
  def on_failed_create_render
309
+ # Ensure resource is set before rendering the new template
310
+ # This prevents Rails 8 deprecation warnings about nil model arguments
311
+ resource = instance_variable_get("@#{resource_var_name}")
312
+ if resource.nil?
313
+ # Always initialize for rendering, even if skip_initialize_create? is true
314
+ # The skip_initialize_create? flag only affects the create action, not rendering
315
+ instance_variable_set("@#{resource_var_name}", new_instance)
316
+ end
219
317
  respond_to do |format|
220
318
  format.turbo_stream { render_action(:new) }
221
- format.html { render :new, status: :unprocessable_entity }
319
+ format.html { render :new, status: UNPROCESSABLE_STATUS }
222
320
  end
223
321
  end
224
322
 
225
323
  def on_failed_update_render
226
324
  respond_to do |format|
227
325
  format.turbo_stream { render_action(:edit) }
228
- format.html { render :edit, status: :unprocessable_entity }
326
+ format.html { render :edit, status: UNPROCESSABLE_STATUS }
229
327
  end
230
328
  end
231
329
 
@@ -246,14 +344,22 @@ module Crudable
246
344
  end
247
345
 
248
346
  def render_action(default_action_name)
249
- logger.debug "Rendering relevant action for #{controller_path}/#{default_action_name} as #{request.format.symbol}"
250
- return render default_action_name if lookup_context.template_exists?("#{controller_path}/#{default_action_name}")
347
+ logger.debug "Rendering relevant action for #{controller_path}/#{default_action_name} " \
348
+ "as #{request.format.symbol}"
349
+ if lookup_context.template_exists?("#{controller_path}/#{default_action_name}")
350
+ return render default_action_name
351
+ end
251
352
 
252
- Crudable::Rails.deprecator.warn("Rendering fallback: #{action_name}, format: #{request.format.symbol}. Rename your template to #{default_action_name}")
353
+ Crudable::Rails.deprecator.warn("Rendering fallback: #{action_name}, format: #{request.format.symbol}. " \
354
+ "Rename your template to #{default_action_name}")
253
355
 
254
356
  logger.debug "Rendering fallback: #{action_name}"
255
357
  render
256
358
  end
359
+
360
+ def render_not_found
361
+ head :not_found
362
+ end
257
363
  end
258
364
  end
259
365
  end
@@ -7,9 +7,8 @@ module Crudable
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  class_methods do
10
- def crudable(nested: false)
10
+ def crudable
11
11
  include Crudable::Rails::Base
12
- include Crudable::Rails::Nestable if nested
13
12
  end
14
13
  end
15
14
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Crudable
2
4
  module Rails
3
5
  class Engine < ::Rails::Engine
@@ -5,7 +7,7 @@ module Crudable
5
7
  ::ActionController::Base.include Crudable::Rails::Controller
6
8
  end
7
9
 
8
- initializer "crudable-rails.deprecator" do |app|
10
+ initializer 'crudable-rails.deprecator' do |app|
9
11
  app.deprecators[:crudable_rails] = Crudable::Rails.deprecator
10
12
  end
11
13
  end
@@ -7,20 +7,89 @@ module Crudable
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  included do
10
- before_action :find_parent, only: %i[index new create]
10
+ before_action :find_parent
11
11
  end
12
12
 
13
13
  def find_parent
14
- params.each do |name, value|
15
- next unless name =~ /(.+)_id$/
16
-
17
- object_name = Regexp.last_match(1)
18
- object_scope = object_name.classify.constantize
19
- object_scope = object_scope.friendly if friendly_finders?
20
- instance_variable_set("@#{object_name}", object_scope.find(value))
21
- self.class.send(:decorates_assigned, object_name.to_sym) if defined?(Draper)
14
+ return unless parent_present?
15
+
16
+ parent_scope = parent_class
17
+ parent_scope = parent_class.friendly if parent_friendly_finders?
18
+ instance_variable_set("@#{parent_var_name}", parent_scope.find(params[parent_id_param]))
19
+ self.class.send(:decorates_assigned, parent_var_name.to_sym) if defined?(Draper)
20
+ rescue ActiveRecord::RecordNotFound => e
21
+ # Only catch and render 404 if we're in a request context
22
+ # Check if we have a request object (indicates we're in a request context)
23
+ raise e unless respond_to?(:request, true) && !request.nil?
24
+
25
+ render_not_found
26
+ nil # Explicitly stop execution to prevent further action execution
27
+ end
28
+
29
+ def parent_id_param
30
+ return nil unless use_parent_as_scope?
31
+
32
+ # Prefer route/path params (ordered by the route definition) over merged params
33
+ # (which may include query params).
34
+ # If multiple *_id params exist (multi-level nesting), default to the deepest/last one.
35
+ @parent_id_param ||= begin
36
+ keys = parent_id_param_keys
37
+ keys.map(&:to_s).grep(/(.+)_id$/).last
22
38
  end
23
39
  end
40
+
41
+ def parent_class
42
+ @parent_class ||= parent_var_name.classify.constantize
43
+ end
44
+
45
+ def parent_var_name
46
+ @parent_var_name ||= parent_id_param.sub(/_id$/, '').underscore
47
+ end
48
+
49
+ def parent_present?
50
+ !parent_id_param.nil?
51
+ end
52
+
53
+ def parent_resource_association
54
+ return unless parent_present?
55
+
56
+ # Handle case where resource_class is a CollectionProxy (association proxy)
57
+ # Extract the actual class for comparison
58
+ actual_resource_class = if resource_class.is_a?(ActiveRecord::Associations::CollectionProxy)
59
+ resource_class.klass
60
+ else
61
+ resource_class
62
+ end
63
+
64
+ association = parent_class.reflect_on_all_associations.find { |assoc| assoc.klass == actual_resource_class }
65
+ association&.name
66
+ end
67
+
68
+ # By default, if friendly finders are enabled for the resource, we will also attempt to use
69
+ # friendly finders for the parent (when available).
70
+ #
71
+ # Override this in your controller if you want to force parent lookup to use (or not use)
72
+ # FriendlyId, independently of the resource.
73
+ def parent_friendly_finders?
74
+ return false unless defined?(FriendlyId)
75
+ return false unless parent_class.respond_to?(:friendly)
76
+
77
+ friendly_finders?
78
+ end
79
+
80
+ def use_parent_as_scope?
81
+ true
82
+ end
83
+
84
+ def parent_id_param_keys
85
+ return request.path_parameters.keys if request_path_parameters_present?
86
+
87
+ params&.keys || []
88
+ end
89
+
90
+ def request_path_parameters_present?
91
+ request.respond_to?(:path_parameters) && request.path_parameters.present?
92
+ end
24
93
  end
25
94
  end
26
95
  end
@@ -31,15 +31,41 @@ module Crudable
31
31
  def resource_namespace
32
32
  namespaces = self.class.name.split('::')
33
33
  namespaces.pop
34
- namespaces.map { |n| n.downcase.to_sym }
34
+ namespaces.map { |n| n.underscore.to_sym }
35
35
  end
36
36
 
37
37
  def authorize_resource(method = action_name)
38
- authorize resource_namespace + [authorizable_resource], "#{method}?".to_sym
38
+ return unless defined?(Pundit)
39
+ # Check if Pundit::Authorization is included (check class, ancestors, and respond_to)
40
+ return unless pundit_authorization_available?
41
+
42
+ authorize resource_namespace + [authorizable_resource], :"#{method}?"
39
43
  end
40
44
 
41
45
  def authorize_class_action(method = action_name)
42
- authorize([*resource_namespace, resource_class], "#{method}?".to_sym)
46
+ return unless defined?(Pundit)
47
+ # Check if Pundit::Authorization is included (check class, ancestors, and respond_to)
48
+ return unless pundit_authorization_available?
49
+
50
+ authorize([*resource_namespace, resource_class], :"#{method}?")
51
+ end
52
+
53
+ def pundit_authorization_available?
54
+ return false unless defined?(Pundit)
55
+ return false unless defined?(Pundit::Authorization)
56
+ # Check if included in this class
57
+ return true if self.class.included_modules.include?(Pundit::Authorization)
58
+
59
+ # Check if included in any ancestor (Class or Module)
60
+ # This handles cases where Pundit::Authorization is included in ApplicationController
61
+ ancestors_to_check = self.class.ancestors.select { |a| a.is_a?(Class) || a.is_a?(Module) }
62
+ return true if ancestors_to_check.any? { |ancestor| ancestor.included_modules.include?(Pundit::Authorization) }
63
+ # Fallback: check if authorize method is available (works in most cases)
64
+ # Use method_defined? for more reliable check than respond_to?
65
+ return true if self.class.method_defined?(:authorize) || self.class.private_method_defined?(:authorize)
66
+
67
+ # Last resort: respond_to check
68
+ respond_to?(:authorize, true)
43
69
  end
44
70
 
45
71
  def authorizable_resource
@@ -3,6 +3,6 @@
3
3
  # Crudable Version
4
4
  module Crudable
5
5
  module Rails
6
- VERSION = '1.3'
6
+ VERSION = '1.4.0'
7
7
  end
8
8
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'turbo-rails'
2
4
 
3
5
  require 'crudable/rails/version'
@@ -10,7 +12,7 @@ require 'crudable/rails/nestable'
10
12
  module Crudable
11
13
  module Rails
12
14
  def self.deprecator
13
- @deprecator ||= ActiveSupport::Deprecation.new("2.0", "Crudable::Rails")
15
+ @deprecator ||= ActiveSupport::Deprecation.new('2.0', 'Crudable::Rails')
14
16
  end
15
17
  end
16
18
  end
@@ -0,0 +1,13 @@
1
+ Description:
2
+ Generates a Rails scaffold and replaces the generated controller with a Crudable controller.
3
+
4
+ Example:
5
+ bin/rails generate crudable:scaffold Thing
6
+
7
+ This will create:
8
+ It will generate a rails scaffold with a Crudable controller.
9
+
10
+ Notes:
11
+ Nested routing is automatically supported: if a parent `*_id` param is present and an association
12
+ exists between the parent and resource, collections and resources will be scoped through the parent.
13
+ Override `use_parent_as_scope?` in your controller to disable parent scoping.
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crudable
4
+ class ScaffoldGenerator < Rails::Generators::NamedBase
5
+ include Rails::Generators::ResourceHelpers
6
+
7
+ source_root File.expand_path('templates', __dir__)
8
+
9
+ argument :attributes, type: :array, default: [], banner: 'field:type field:type'
10
+
11
+ hook_for :scaffold_controller, in: :rails
12
+
13
+ def crudable
14
+ template 'crudable_controller.rb',
15
+ File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb"), force: true
16
+ end
17
+
18
+ private
19
+
20
+ def permitted_params
21
+ attachments, others = attributes_names.partition { |name| attachments?(name) }
22
+ params = others.map { |name| ":#{name}" }
23
+ params += attachments.map { |name| "#{name}: []" }
24
+ params.join(', ')
25
+ end
26
+
27
+ def attachments?(name)
28
+ attribute = attributes.find { |attr| attr.name == name }
29
+ attribute&.attachments?
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,164 @@
1
+ <% module_namespacing do -%>
2
+ class <%= controller_class_name %>Controller < ApplicationController
3
+ crudable
4
+
5
+ private
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Optional overrides (self-documenting hooks)
9
+ #
10
+ # `crudable` mixes in `Crudable::Rails::Base` (which includes `Nestable`),
11
+ # which provides the REST actions and calls these hooks. Uncomment and tailor
12
+ # any of the following as needed.
13
+ # ---------------------------------------------------------------------------
14
+ #
15
+ # # If this resource is singular (e.g. SettingsController), return true.
16
+ # def singleton?
17
+ # false
18
+ # end
19
+ #
20
+ # # Which param key to use when finding a member resource (default :id).
21
+ # # Common examples: :slug, :uuid
22
+ # def finder_param
23
+ # :id
24
+ # end
25
+ #
26
+ # # Enable FriendlyId finders/redirects (default false).
27
+ # def friendly_finders?
28
+ # false
29
+ # end
30
+ #
31
+ # # Nested parent lookup: use FriendlyId for the parent (defaults to friendly_finders?).
32
+ # # This is only applied when FriendlyId is available and the parent model responds to `.friendly`.
33
+ # def parent_friendly_finders?
34
+ # friendly_finders?
35
+ # end
36
+ #
37
+ # # Enable pagination (requires Kaminari) (default true when Kaminari is defined).
38
+ # def paginate_resource?
39
+ # super
40
+ # end
41
+ #
42
+ # # Customize the scope used for index. This is the primary hook for filtering.
43
+ # # (HasScope is applied automatically if present.)
44
+ # def authorizable_scope
45
+ # super
46
+ # end
47
+ #
48
+ # # Full scope used for index (after HasScope, Pundit, etc).
49
+ # def resource_scope
50
+ # super
51
+ # end
52
+ #
53
+ # # Skip initializing a new instance in create (useful for custom create flows).
54
+ # def skip_initialize_create?
55
+ # false
56
+ # end
57
+ #
58
+ # # Discard/soft-delete support (requires Discard). When true, destroy will
59
+ # # choose discard vs destroy based on record state and params.
60
+ # def discard?
61
+ # false
62
+ # end
63
+ #
64
+ # # Control whether Pundit authorization should be performed.
65
+ # # Override this to customize authorization behavior (e.g., feature flags, user roles).
66
+ # # Default: returns true when Pundit is defined.
67
+ # def authorize_with_pundit?
68
+ # super
69
+ # end
70
+ #
71
+ # # Authorization lifecycle hooks (called inside create/update).
72
+ # def before_authorize_create; end
73
+ # def after_authorize_create; end
74
+ # def before_authorize_update; end
75
+ # def after_authorize_update; end
76
+ #
77
+ # # Post-success hooks (side-effects, instrumentation, etc).
78
+ # def on_successful_create; end
79
+ # def on_successful_update; end
80
+ #
81
+ # # Failure setup hooks (e.g., rebuild form collections).
82
+ # def on_failed_create_setup; end
83
+ # def on_failed_update_setup; end
84
+ #
85
+ # # Customize success redirects/flash messages.
86
+ # def after_create_redirect_path
87
+ # super
88
+ # end
89
+ #
90
+ # def after_create_notice
91
+ # super
92
+ # end
93
+ #
94
+ # def after_update_redirect_path
95
+ # super
96
+ # end
97
+ #
98
+ # def after_update_notice
99
+ # super
100
+ # end
101
+ #
102
+ # def after_destroy_redirect_path
103
+ # super
104
+ # end
105
+ #
106
+ # def after_failed_destroy_redirect_path
107
+ # super
108
+ # end
109
+ #
110
+ # def after_destroy_notice
111
+ # super
112
+ # end
113
+ #
114
+ # def after_failed_destroy_alert
115
+ # super
116
+ # end
117
+ #
118
+ # # Customize rendering for failed creates/updates (Turbo/HTML).
119
+ # def on_failed_create_render
120
+ # super
121
+ # end
122
+ #
123
+ # def on_failed_update_render
124
+ # super
125
+ # end
126
+ #
127
+ # # Customize rendering for destroy outcomes.
128
+ # def on_successful_destroy_render
129
+ # super
130
+ # end
131
+ #
132
+ # def on_failed_destroy_render
133
+ # super
134
+ # end
135
+ #
136
+ # # Nested resources: parent scoping is automatic when a `*_id` param is present.
137
+ # # Override `use_parent_as_scope?` to disable parent scoping.
138
+ # def find_parent
139
+ # super
140
+ # end
141
+ #
142
+ # def use_parent_as_scope?
143
+ # true
144
+ # end
145
+ #
146
+ # # Nested resources: customize which `*_id` param is treated as the parent id.
147
+ # # Useful for multi-level nesting or non-standard param names.
148
+ # def parent_id_param
149
+ # super
150
+ # end
151
+ #
152
+ # Strong params used by `create` (and by default `update` via `update_params`).
153
+ # Adjust the permitted attributes to match your model and nested params shape.
154
+ def create_params
155
+ <%- if attributes_names.empty? -%>
156
+ params.fetch(:<%= singular_table_name %>, {})
157
+ <%- else -%>
158
+ params.expect(<%= singular_table_name %>: [ <%= permitted_params %> ])
159
+ <%- end -%>
160
+ end
161
+ # By default, updates permit the same attributes as creates.
162
+ alias update_params create_params
163
+ end
164
+ <% end -%>
@@ -0,0 +1,13 @@
1
+ Description:
2
+ Extends the Rails scaffold controller generator with an option to generate a Crudable controller.
3
+
4
+ Example:
5
+ bin/rails generate scaffold Thing
6
+
7
+ This will create:
8
+ It will invoke the rails scaffold controller with an optional Crudable controller (use `--crudable`).
9
+
10
+ Notes:
11
+ Nested routing is automatically supported: if a parent `*_id` param is present and an association
12
+ exists between the parent and resource, collections and resources will be scoped through the parent.
13
+ Override `use_parent_as_scope?` in your controller to disable parent scoping.
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ScaffoldControllerGenerator < Rails::Generators::NamedBase
4
+ include Rails::Generators::ResourceHelpers
5
+
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ class_exclusive do
9
+ class_option :crudable, type: :boolean, desc: 'Generate with crudable controller'
10
+ end
11
+
12
+ argument :attributes, type: :array, default: [], banner: 'field:type field:type'
13
+
14
+ hook_for :scaffold_controller, in: :rails
15
+
16
+ def crudable
17
+ return unless options.crudable?
18
+
19
+ template 'crudable_controller.rb',
20
+ File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb"), force: true
21
+ end
22
+
23
+ private
24
+
25
+ def permitted_params
26
+ attachments, others = attributes_names.partition { |name| attachments?(name) }
27
+ params = others.map { |name| ":#{name}" }
28
+ params += attachments.map { |name| "#{name}: []" }
29
+ params.join(', ')
30
+ end
31
+
32
+ def attachments?(name)
33
+ attribute = attributes.find { |attr| attr.name == name }
34
+ attribute&.attachments?
35
+ end
36
+ end
@@ -0,0 +1,164 @@
1
+ <% module_namespacing do -%>
2
+ class <%= controller_class_name %>Controller < ApplicationController
3
+ crudable
4
+
5
+ private
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Optional overrides (self-documenting hooks)
9
+ #
10
+ # `crudable` mixes in `Crudable::Rails::Base` (which includes `Nestable`),
11
+ # which provides the REST actions and calls these hooks. Uncomment and tailor
12
+ # any of the following as needed.
13
+ # ---------------------------------------------------------------------------
14
+ #
15
+ # # If this resource is singular (e.g. SettingsController), return true.
16
+ # def singleton?
17
+ # false
18
+ # end
19
+ #
20
+ # # Which param key to use when finding a member resource (default :id).
21
+ # # Common examples: :slug, :uuid
22
+ # def finder_param
23
+ # :id
24
+ # end
25
+ #
26
+ # # Enable FriendlyId finders/redirects (default false).
27
+ # def friendly_finders?
28
+ # false
29
+ # end
30
+ #
31
+ # # Nested parent lookup: use FriendlyId for the parent (defaults to friendly_finders?).
32
+ # # This is only applied when FriendlyId is available and the parent model responds to `.friendly`.
33
+ # def parent_friendly_finders?
34
+ # friendly_finders?
35
+ # end
36
+ #
37
+ # # Enable pagination (requires Kaminari) (default true when Kaminari is defined).
38
+ # def paginate_resource?
39
+ # super
40
+ # end
41
+ #
42
+ # # Customize the scope used for index. This is the primary hook for filtering.
43
+ # # (HasScope is applied automatically if present.)
44
+ # def authorizable_scope
45
+ # super
46
+ # end
47
+ #
48
+ # # Full scope used for index (after HasScope, Pundit, etc).
49
+ # def resource_scope
50
+ # super
51
+ # end
52
+ #
53
+ # # Skip initializing a new instance in create (useful for custom create flows).
54
+ # def skip_initialize_create?
55
+ # false
56
+ # end
57
+ #
58
+ # # Discard/soft-delete support (requires Discard). When true, destroy will
59
+ # # choose discard vs destroy based on record state and params.
60
+ # def discard?
61
+ # false
62
+ # end
63
+ #
64
+ # # Control whether Pundit authorization should be performed.
65
+ # # Override this to customize authorization behavior (e.g., feature flags, user roles).
66
+ # # Default: returns true when Pundit is defined.
67
+ # def authorize_with_pundit?
68
+ # super
69
+ # end
70
+ #
71
+ # # Authorization lifecycle hooks (called inside create/update).
72
+ # def before_authorize_create; end
73
+ # def after_authorize_create; end
74
+ # def before_authorize_update; end
75
+ # def after_authorize_update; end
76
+ #
77
+ # # Post-success hooks (side-effects, instrumentation, etc).
78
+ # def on_successful_create; end
79
+ # def on_successful_update; end
80
+ #
81
+ # # Failure setup hooks (e.g., rebuild form collections).
82
+ # def on_failed_create_setup; end
83
+ # def on_failed_update_setup; end
84
+ #
85
+ # # Customize success redirects/flash messages.
86
+ # def after_create_redirect_path
87
+ # super
88
+ # end
89
+ #
90
+ # def after_create_notice
91
+ # super
92
+ # end
93
+ #
94
+ # def after_update_redirect_path
95
+ # super
96
+ # end
97
+ #
98
+ # def after_update_notice
99
+ # super
100
+ # end
101
+ #
102
+ # def after_destroy_redirect_path
103
+ # super
104
+ # end
105
+ #
106
+ # def after_failed_destroy_redirect_path
107
+ # super
108
+ # end
109
+ #
110
+ # def after_destroy_notice
111
+ # super
112
+ # end
113
+ #
114
+ # def after_failed_destroy_alert
115
+ # super
116
+ # end
117
+ #
118
+ # # Customize rendering for failed creates/updates (Turbo/HTML).
119
+ # def on_failed_create_render
120
+ # super
121
+ # end
122
+ #
123
+ # def on_failed_update_render
124
+ # super
125
+ # end
126
+ #
127
+ # # Customize rendering for destroy outcomes.
128
+ # def on_successful_destroy_render
129
+ # super
130
+ # end
131
+ #
132
+ # def on_failed_destroy_render
133
+ # super
134
+ # end
135
+ #
136
+ # # Nested resources: parent scoping is automatic when a `*_id` param is present.
137
+ # # Override `use_parent_as_scope?` to disable parent scoping.
138
+ # def find_parent
139
+ # super
140
+ # end
141
+ #
142
+ # def use_parent_as_scope?
143
+ # true
144
+ # end
145
+ #
146
+ # # Nested resources: customize which `*_id` param is treated as the parent id.
147
+ # # Useful for multi-level nesting or non-standard param names.
148
+ # def parent_id_param
149
+ # super
150
+ # end
151
+ #
152
+ # Strong params used by `create` (and by default `update` via `update_params`).
153
+ # Adjust the permitted attributes to match your model and nested params shape.
154
+ def create_params
155
+ <%- if attributes_names.empty? -%>
156
+ params.fetch(:<%= singular_table_name %>, {})
157
+ <%- else -%>
158
+ params.expect(<%= singular_table_name %>: [ <%= permitted_params %> ])
159
+ <%- end -%>
160
+ end
161
+ # By default, updates permit the same attributes as creates.
162
+ alias update_params create_params
163
+ end
164
+ <% end -%>
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # desc "Explaining what the task does"
2
4
  # task :crudable do
3
5
  # # Task goes here
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crudable-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.3'
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tomislav Simnett
@@ -15,7 +15,7 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '7.0'
18
+ version: 7.1.0
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: 9.0.0
@@ -25,7 +25,7 @@ dependencies:
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: '7.0'
28
+ version: 7.1.0
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: 9.0.0
@@ -44,45 +44,45 @@ dependencies:
44
44
  - !ruby/object:Gem::Version
45
45
  version: 2.0.0
46
46
  - !ruby/object:Gem::Dependency
47
- name: rubocop
47
+ name: mocha
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  requirements:
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
- version: '1.50'
52
+ version: '2.6'
53
53
  - - "<"
54
54
  - !ruby/object:Gem::Version
55
- version: '2.0'
55
+ version: 3.0.0
56
56
  type: :development
57
57
  prerelease: false
58
58
  version_requirements: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - ">="
61
61
  - !ruby/object:Gem::Version
62
- version: '1.50'
62
+ version: '2.6'
63
63
  - - "<"
64
64
  - !ruby/object:Gem::Version
65
- version: '2.0'
65
+ version: 3.0.0
66
66
  - !ruby/object:Gem::Dependency
67
- name: mocha
67
+ name: rubocop
68
68
  requirement: !ruby/object:Gem::Requirement
69
69
  requirements:
70
70
  - - ">="
71
71
  - !ruby/object:Gem::Version
72
- version: '2.6'
72
+ version: '1.50'
73
73
  - - "<"
74
74
  - !ruby/object:Gem::Version
75
- version: 3.0.0
75
+ version: '2.0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: '2.6'
82
+ version: '1.50'
83
83
  - - "<"
84
84
  - !ruby/object:Gem::Version
85
- version: 3.0.0
85
+ version: '2.0'
86
86
  description: Crudable Rails provides everything needed to quickly build fully functional
87
87
  CRUD-based controllers in a Rails application. It streamlines resource management,
88
88
  reduces boilerplate, and enforces best practices for handling requests, responses,
@@ -105,11 +105,18 @@ files:
105
105
  - lib/crudable/rails/nestable.rb
106
106
  - lib/crudable/rails/resourceable.rb
107
107
  - lib/crudable/rails/version.rb
108
+ - lib/generators/crudable/scaffold/USAGE
109
+ - lib/generators/crudable/scaffold/scaffold_generator.rb
110
+ - lib/generators/crudable/scaffold/templates/crudable_controller.rb.tt
111
+ - lib/generators/scaffold_controller/USAGE
112
+ - lib/generators/scaffold_controller/scaffold_controller_generator.rb
113
+ - lib/generators/scaffold_controller/templates/crudable_controller.rb.tt
108
114
  - lib/tasks/crudable_tasks.rake
109
115
  homepage: https://gitlab.com/initforthe/crudable-rails
110
116
  licenses:
111
117
  - MIT
112
- metadata: {}
118
+ metadata:
119
+ rubygems_mfa_required: 'true'
113
120
  rdoc_options: []
114
121
  require_paths:
115
122
  - lib
@@ -124,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
124
131
  - !ruby/object:Gem::Version
125
132
  version: '0'
126
133
  requirements: []
127
- rubygems_version: 3.7.1
134
+ rubygems_version: 4.0.12
128
135
  specification_version: 4
129
136
  summary: CRUD operations for Rails controllers
130
137
  test_files: []