rest_framework 1.0.2 → 1.2.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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -26
  3. data/VERSION +1 -1
  4. data/app/views/rest_framework/_routes_and_forms.html.erb +2 -5
  5. data/app/views/rest_framework/routes_and_forms/_html_form.html.erb +1 -1
  6. data/app/views/rest_framework/routes_and_forms/_raw_form.html.erb +1 -1
  7. data/lib/rest_framework/controller/bulk.rb +272 -0
  8. data/lib/rest_framework/controller/crud.rb +65 -0
  9. data/lib/rest_framework/controller/openapi.rb +252 -0
  10. data/lib/rest_framework/controller.rb +839 -0
  11. data/lib/rest_framework/engine.rb +12 -2
  12. data/lib/rest_framework/errors.rb +53 -4
  13. data/lib/rest_framework/filters/ordering_filter.rb +0 -1
  14. data/lib/rest_framework/filters/query_filter.rb +7 -2
  15. data/lib/rest_framework/filters/search_filter.rb +5 -5
  16. data/lib/rest_framework/mixins/base_controller_mixin.rb +3 -383
  17. data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +27 -68
  18. data/lib/rest_framework/mixins/model_controller_mixin.rb +60 -807
  19. data/lib/rest_framework/paginators/page_number_paginator.rb +10 -11
  20. data/lib/rest_framework/routers.rb +20 -9
  21. data/lib/rest_framework/serializers/native_serializer.rb +5 -3
  22. data/lib/rest_framework/utils.rb +24 -6
  23. data/lib/rest_framework.rb +13 -5
  24. metadata +6 -7
  25. data/lib/rest_framework/errors/base_error.rb +0 -5
  26. data/lib/rest_framework/errors/nil_passed_to_render_api_error.rb +0 -14
  27. data/lib/rest_framework/errors/unknown_model_error.rb +0 -18
  28. data/lib/rest_framework/generators/controller_generator.rb +0 -64
  29. data/lib/rest_framework/generators.rb +0 -4
@@ -13,22 +13,21 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
13
13
  end
14
14
 
15
15
  def _page_size
16
- page_size = 1
16
+ page_size = nil
17
17
 
18
- # Get from context, if allowed.
18
+ # Get from query param, if allowed.
19
19
  if param = @controller.class.page_size_query_param
20
- if page_size = @controller.params[param].presence
21
- page_size = page_size.to_i
20
+ if raw = @controller.params[param].presence
21
+ parsed = raw.to_i
22
+ page_size = parsed if parsed > 0
22
23
  end
23
24
  end
24
25
 
25
- # Otherwise, get from config.
26
- if !page_size && @controller.class.page_size
27
- page_size = @controller.class.page_size.to_i
28
- end
26
+ # Fall back to the configured page size.
27
+ page_size ||= @controller.class.page_size&.to_i || 1
29
28
 
30
29
  # Ensure we don't exceed the max page size.
31
- max_page_size = @controller.class.max_page_size&.to_i
30
+ max_page_size = @controller.class.max_page_size
32
31
  if max_page_size && page_size > max_page_size
33
32
  page_size = max_page_size
34
33
  end
@@ -46,7 +45,7 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
46
45
  page_number = 1
47
46
  else
48
47
  page_number = page_number.to_i
49
- if page_number.zero?
48
+ if page_number < 1
50
49
  page_number = 1
51
50
  end
52
51
  end
@@ -61,7 +60,7 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
61
60
  # Wrap the serialized page with appropriate metadata.
62
61
  def get_paginated_response(serialized_page)
63
62
  page_query_param = @controller.class.page_query_param
64
- base_params = @controller.params.to_unsafe_h
63
+ base_params = @controller.request.query_parameters.symbolize_keys
65
64
  next_url = if @page_number < @total_pages
66
65
  @controller.url_for({ **base_params, page_query_param => @page_number + 1 })
67
66
  end
@@ -31,7 +31,7 @@ module ActionDispatch::Routing
31
31
 
32
32
  begin
33
33
  controller = mod.const_get(name_reverse)
34
- rescue
34
+ rescue NameError
35
35
  reraise = true
36
36
  end
37
37
 
@@ -109,16 +109,25 @@ module ActionDispatch::Routing
109
109
  next unless controller_class.method_defined?(action)
110
110
 
111
111
  [ methods ].flatten.each do |m|
112
- public_send(m, "", action: action) if self.respond_to?(m)
112
+ # Anchor the route since Rails 8.1 OPTIONS routes are non-anchored by default, which
113
+ # causes parent OPTIONS routes to greedily intercept sub-path requests.
114
+ public_send(m, "", action: action, anchor: true) if self.respond_to?(m)
113
115
  end
114
116
  end
115
117
 
116
- # Route bulk actions, if configured.
117
- RESTFramework::RRF_BUILTIN_BULK_ACTIONS.each do |action, methods|
118
- next unless controller_class.method_defined?(action)
119
-
120
- [ methods ].flatten.each do |m|
121
- public_send(m, "", action: action) if self.respond_to?(m)
118
+ # Route bulk actions, if configured. These require a model and are gated by the `bulk`
119
+ # attribute, and may be individually excluded via `excluded_actions`.
120
+ if controller_class.model && controller_class.bulk
121
+ bulk_exclude = controller_class.excluded_actions&.to_set || Set.new
122
+ RESTFramework::RRF_BUILTIN_BULK_ACTIONS.each do |action, methods|
123
+ next unless controller_class.method_defined?(action)
124
+ next if bulk_exclude.include?(action)
125
+
126
+ [ methods ].flatten.each do |m|
127
+ # Anchor the route since Rails 8.1 OPTIONS routes are non-anchored by default, which
128
+ # causes parent OPTIONS routes to greedily intercept sub-path requests.
129
+ public_send(m, "", action: action, anchor: true) if self.respond_to?(m)
130
+ end
122
131
  end
123
132
  end
124
133
  end
@@ -179,7 +188,9 @@ module ActionDispatch::Routing
179
188
  next unless controller_class.method_defined?(action)
180
189
 
181
190
  [ methods ].flatten.each do |m|
182
- public_send(m, "", action: action) if self.respond_to?(m)
191
+ # Anchor the route since Rails 8.1 OPTIONS routes are non-anchored by default, which
192
+ # causes parent OPTIONS routes to greedily intercept sub-path requests.
193
+ public_send(m, "", action: action, anchor: true) if self.respond_to?(m)
183
194
  end
184
195
  end
185
196
  end
@@ -28,10 +28,12 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
28
28
  # Determine model either explicitly, or by inspecting @object or @controller.
29
29
  @model = model
30
30
  @model ||= @object.class if @object.is_a?(ActiveRecord::Base)
31
- @model ||= @object[0].class if
32
- @many && @object.is_a?(Enumerable) && @object.is_a?(ActiveRecord::Base)
31
+ @model ||= @object.klass if @many && @object.is_a?(ActiveRecord::Relation)
32
+ @model ||= @object.first.class if @many &&
33
+ @object.is_a?(Enumerable) &&
34
+ @object.first.is_a?(ActiveRecord::Base)
33
35
 
34
- @model ||= @controller.class.get_model if @controller
36
+ @model ||= @controller.class.model if @controller
35
37
  end
36
38
 
37
39
  def action
@@ -42,17 +42,27 @@ module RESTFramework::Utils
42
42
  end
43
43
 
44
44
  def self.get_skipped_builtin_actions(controller_class, singular)
45
- (
46
- (
47
- RESTFramework::BUILTIN_ACTIONS.keys - (singular ? [ :index ] : [])
48
- ) + RESTFramework::BUILTIN_MEMBER_ACTIONS.keys
49
- ).reject do |action|
50
- controller_class.method_defined?(action)
45
+ candidates = (
46
+ RESTFramework::BUILTIN_ACTIONS.keys - (singular ? [ :index ] : [])
47
+ ) + RESTFramework::BUILTIN_MEMBER_ACTIONS.keys
48
+
49
+ return candidates unless controller_class.model
50
+
51
+ exclude = controller_class.excluded_actions&.to_set || Set.new
52
+ candidates.reject do |action|
53
+ controller_class.method_defined?(action) && !exclude.include?(action)
51
54
  end
52
55
  end
53
56
 
54
57
  # Get the first route pattern which matches the given request.
55
58
  def self.get_request_route(application_routes, request)
59
+ # Prefer the route already resolved by the router to avoid an expensive `recognize` call. This
60
+ # is also required for Rails 8.1+ where OPTIONS routes are non-anchored, causing `path_info` to
61
+ # be modified during dispatch, which makes `recognize` fail from inside the controller action.
62
+ if route = request.env["action_dispatch.route"]
63
+ return route
64
+ end
65
+
56
66
  application_routes.router.recognize(request) { |route, _| return route }
57
67
  end
58
68
 
@@ -241,4 +251,12 @@ module RESTFramework::Utils
241
251
 
242
252
  s
243
253
  end
254
+
255
+ # Used for deprecated mixins that rely on model being determined from the controller name.
256
+ def self.get_model(controller_class)
257
+ begin
258
+ controller_class.name.demodulize.chomp("Controller").singularize.constantize
259
+ rescue NameError
260
+ end
261
+ end
244
262
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RESTFramework
4
- BUILTIN_FORM_ACTIONS = %i[new edit].freeze
4
+ BUILTIN_FORM_ACTIONS = [ :new, :edit ].freeze
5
5
  BUILTIN_ACTIONS = {
6
6
  index: :get,
7
7
  new: :get,
@@ -144,6 +144,7 @@ module RESTFramework
144
144
  password
145
145
  password_confirmation
146
146
  ].freeze
147
+ DEFAULT_INFLECT_ACRONYMS = [ "ID", "IDs", "REST", "API", "APIs" ].freeze
147
148
 
148
149
  # Permits use of `render(api: obj)` syntax over `render_api(obj)`; `true` by default.
149
150
  attr_accessor :register_api_renderer
@@ -168,9 +169,6 @@ module RESTFramework
168
169
  # Whether the backtrace should be shown in rescued errors.
169
170
  attr_accessor :show_backtrace
170
171
 
171
- # Disable `rescue_from` on the controller mixins.
172
- attr_accessor :disable_rescue_from
173
-
174
172
  # The default label fields to use when generating labels for `has_many` associations.
175
173
  attr_accessor :label_fields
176
174
 
@@ -181,6 +179,9 @@ module RESTFramework
181
179
  attr_accessor :read_only_fields
182
180
  attr_accessor :write_only_fields
183
181
 
182
+ # List of acronyms to be inflected in controller titles and field labels.
183
+ attr_accessor :inflect_acronyms
184
+
184
185
  # Option to use vendored assets (requires sprockets or propshaft) rather than linking to
185
186
  # external assets (the default).
186
187
  attr_accessor :use_vendored_assets
@@ -188,6 +189,7 @@ module RESTFramework
188
189
  def initialize
189
190
  self.register_api_renderer = true
190
191
  self.auto_finalize = true
192
+ self.freeze_config = true
191
193
 
192
194
  self.show_backtrace = Rails.env.development?
193
195
 
@@ -195,6 +197,7 @@ module RESTFramework
195
197
  self.search_columns = DEFAULT_SEARCH_COLUMNS
196
198
  self.read_only_fields = DEFAULT_READ_ONLY_FIELDS
197
199
  self.write_only_fields = DEFAULT_WRITE_ONLY_FIELDS
200
+ self.inflect_acronyms = DEFAULT_INFLECT_ACRONYMS
198
201
  end
199
202
  end
200
203
 
@@ -209,15 +212,20 @@ module RESTFramework
209
212
  def self.features
210
213
  @features ||= {}
211
214
  end
215
+
216
+ def self.deprecator
217
+ @deprecator ||= ActiveSupport::Deprecation.new("2.0", "REST Framework")
218
+ end
212
219
  end
213
220
 
214
221
  require_relative "rest_framework/engine"
215
222
  require_relative "rest_framework/errors"
216
223
  require_relative "rest_framework/filters"
217
- require_relative "rest_framework/generators"
218
224
  require_relative "rest_framework/mixins"
219
225
  require_relative "rest_framework/paginators"
220
226
  require_relative "rest_framework/routers"
221
227
  require_relative "rest_framework/serializers"
222
228
  require_relative "rest_framework/utils"
223
229
  require_relative "rest_framework/version"
230
+
231
+ require_relative "rest_framework/controller"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rest_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregory N. Schmit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-13 00:00:00.000000000 Z
11
+ date: 2026-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -54,19 +54,18 @@ files:
54
54
  - app/views/rest_framework/routes_and_forms/_routes.html.erb
55
55
  - app/views/rest_framework/routes_and_forms/routes/_route.html.erb
56
56
  - lib/rest_framework.rb
57
+ - lib/rest_framework/controller.rb
58
+ - lib/rest_framework/controller/bulk.rb
59
+ - lib/rest_framework/controller/crud.rb
60
+ - lib/rest_framework/controller/openapi.rb
57
61
  - lib/rest_framework/engine.rb
58
62
  - lib/rest_framework/errors.rb
59
- - lib/rest_framework/errors/base_error.rb
60
- - lib/rest_framework/errors/nil_passed_to_render_api_error.rb
61
- - lib/rest_framework/errors/unknown_model_error.rb
62
63
  - lib/rest_framework/filters.rb
63
64
  - lib/rest_framework/filters/base_filter.rb
64
65
  - lib/rest_framework/filters/ordering_filter.rb
65
66
  - lib/rest_framework/filters/query_filter.rb
66
67
  - lib/rest_framework/filters/ransack_filter.rb
67
68
  - lib/rest_framework/filters/search_filter.rb
68
- - lib/rest_framework/generators.rb
69
- - lib/rest_framework/generators/controller_generator.rb
70
69
  - lib/rest_framework/mixins.rb
71
70
  - lib/rest_framework/mixins/base_controller_mixin.rb
72
71
  - lib/rest_framework/mixins/bulk_model_controller_mixin.rb
@@ -1,5 +0,0 @@
1
- class RESTFramework::Errors::BaseError < StandardError
2
- end
3
-
4
- # Alias for convenience.
5
- RESTFramework::BaseError = RESTFramework::Errors::BaseError
@@ -1,14 +0,0 @@
1
- class RESTFramework::Errors::NilPassedToRenderAPIError < RESTFramework::Errors::BaseError
2
- def message
3
- <<~MSG.split("\n").join(" ")
4
- Payload of `nil` was passed to `render_api`; this is unsupported. If you want a blank
5
- response, pass `''` (an empty string) as the payload. If this was the result of a `find_by`
6
- (or similar Active Record method) not finding a record, you should use the bang version (e.g.,
7
- `find_by!`) to raise `ActiveRecord::RecordNotFound`, which the REST controller will catch and
8
- return an appropriate error response.
9
- MSG
10
- end
11
- end
12
-
13
- # Alias for convenience.
14
- RESTFramework::NilPassedToRenderAPIError = RESTFramework::Errors::NilPassedToRenderAPIError
@@ -1,18 +0,0 @@
1
- class RESTFramework::Errors::UnknownModelError < RESTFramework::Errors::BaseError
2
- def initialize(controller_class)
3
- super()
4
- @controller_class = controller_class
5
- end
6
-
7
- def message
8
- <<~MSG.split("\n").join(" ")
9
- The model class for `#{@controller_class}` could not be determined. Any controller that
10
- includes `RESTFramework::BaseModelControllerMixin` (directly or indirectly) must either set
11
- the `model` attribute on the controller, or the model must be deducible from the controller
12
- name (e.g., `UsersController` could resolve to the `User` model).
13
- MSG
14
- end
15
- end
16
-
17
- # Alias for convenience.
18
- RESTFramework::UnknownModelError = RESTFramework::Errors::UnknownModelError
@@ -1,64 +0,0 @@
1
- require "rails/generators"
2
-
3
- # Most projects don't have the inflection "REST" as an acronym, so this is a helper class to prevent
4
- # this generator from being namespaced as `"r_e_s_t_framework"`.
5
- # :nocov:
6
- class RESTFrameworkCustomGeneratorControllerNamespace < String
7
- def camelize
8
- "RESTFramework"
9
- end
10
- end
11
- # :nocov:
12
-
13
- class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
14
- PATH_REGEX = %r{^[a-z0-9][a-z0-9_/]+$}
15
-
16
- desc <<~END
17
- Description:
18
- Generates a new REST Framework controller.
19
-
20
- Specify the controller as a path, including the module, if needed, like:
21
- 'parent_module/controller_name'.
22
-
23
- Example:
24
- `rails generate rest_framework:controller user_api/groups`
25
-
26
- Generates a controller at `app/controllers/user_api/groups_controller.rb` named
27
- `UserApi::GroupsController`.
28
- END
29
-
30
- argument :path, type: :string
31
- class_option(
32
- :parent_class, type: :string, default: "ApplicationController", desc: "Inheritance parent"
33
- )
34
- class_option(
35
- :include_base,
36
- type: :boolean,
37
- default: false,
38
- desc: "Include `BaseControllerMixin`, not `ModelControllerMixin`",
39
- )
40
-
41
- # Some projects may not have the inflection "REST" as an acronym, which changes this generator to
42
- # be namespaced in `r_e_s_t_framework`, which is weird.
43
- def self.namespace
44
- RESTFrameworkCustomGeneratorControllerNamespace.new("rest_framework:controller")
45
- end
46
-
47
- def create_rest_controller_file
48
- unless PATH_REGEX.match?(self.path)
49
- raise StandardError, "Path isn't valid."
50
- end
51
-
52
- # Remove '_controller' from end of path, if it exists.
53
- cleaned_path = self.path.delete_suffix("_controller")
54
-
55
- content = <<~END
56
- class #{cleaned_path.camelize}Controller < #{options[:parent_class]}
57
- include RESTFramework::#{
58
- options[:include_base] ? "BaseControllerMixin" : "ModelControllerMixin"
59
- }
60
- end
61
- END
62
- create_file("app/controllers/#{cleaned_path}_controller.rb", content)
63
- end
64
- end
@@ -1,4 +0,0 @@
1
- module RESTFramework::Generators
2
- end
3
-
4
- require_relative "generators/controller_generator"