rest_framework 0.9.8 → 0.9.10

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: 5b06a61592498f8debce6f504b4eee90ceeef6fdff63dbb769bd70c623cd9631
4
- data.tar.gz: ab5b1284a3504104b77cc38278bad01a1dfa30ce7ebcd65323b73e0dd31e58a2
3
+ metadata.gz: 2db255f618d449ef621f1337d182993c21b5d8d87e5e8c960c8b42093248c91d
4
+ data.tar.gz: 4612f2e11733ae69e418cd2877b8150e2a1dbd4fb0cd34ed29727dcec007f305
5
5
  SHA512:
6
- metadata.gz: ed04505314f252ac1058732a875711090fb07499edac42814bf72e5b15f90f833040e40399df279654727041af30789c8a931e06d83b9fc1c67f46d4f0da850f
7
- data.tar.gz: abd68ae8ab71f2fff8f7a933270a467811143096f7cdde16d21c99ac3faeadc8f958cb0bcdde94c1b57f1d620fc278f8d9b5e3b4e2cec5851fb4616f9c325d41
6
+ metadata.gz: 04457aa989c261900e8e1e55829dbb3cc3aed3afda800433944497996a330a92fdff403351be8e4b83b7b44fea658ecd57a3ab20b4b9e41587398a52b8203680
7
+ data.tar.gz: e67d99fd29629b7bf31e37f3f27edc8e7d5d222172e6b91e5d2340eec0d891b340029097dc798411410d9c5ecbdffe316521c0dd47e8810c6fe897b0ff59624b
data/README.md CHANGED
@@ -7,12 +7,10 @@
7
7
 
8
8
  A framework for DRY RESTful APIs in Ruby on Rails.
9
9
 
10
- **The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD
11
- logic, and routing them can be obnoxious. Building and maintaining features like ordering,
12
- filtering, and pagination can be tedious.
10
+ **The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD logic, and routing them can be obnoxious.
11
+ Building and maintaining features like ordering, filtering, and pagination can be tedious.
13
12
 
14
- **The Solution**: This framework implements browsable API responses, CRUD actions for your models,
15
- and features like ordering/filtering/pagination, so you can focus on building awesome APIs.
13
+ **The Solution**: This framework implements browsable API responses, CRUD actions for your models, and features like ordering/filtering/pagination, so you can focus on building awesome APIs.
16
14
 
17
15
  Website/Guide: [rails-rest-framework.com](https://rails-rest-framework.com)
18
16
 
@@ -27,87 +25,119 @@ YARD Docs: [rubydoc.info/gems/rest_framework](https://rubydoc.info/gems/rest_fra
27
25
  Add this line to your application's Gemfile:
28
26
 
29
27
  ```ruby
30
- gem 'rest_framework'
28
+ gem "rest_framework"
31
29
  ```
32
30
 
33
- And then execute:
31
+ And then run:
34
32
 
35
33
  ```shell
36
- $ bundle install
34
+ bundle install
37
35
  ```
38
36
 
39
- Or install it yourself with:
37
+ ## Quick Usage Tutorial
40
38
 
41
- ```shell
42
- $ gem install rest_framework
43
- ```
39
+ This section provides some simple examples to quickly get you started using the framework.
44
40
 
45
- ## Quick Usage Tutorial
41
+ For the purpose of this example, you'll want to add an `api_controller.rb` to your controllers, as well as a directory for the resources:
42
+
43
+ ```text
44
+ controllers/
45
+ ├─ api_controller.rb
46
+ └─ api/
47
+ ├─ root_controller.rb
48
+ ├─ movies_controller.rb
49
+ └─ users_controller.rb
50
+ ```
46
51
 
47
52
  ### Controller Mixins
48
53
 
49
- To transform a controller into a RESTful controller, you can either include `BaseControllerMixin`,
50
- `ReadOnlyModelControllerMixin`, or `ModelControllerMixin`. `BaseControllerMixin` provides a `root`
51
- action and a simple interface for routing arbitrary additional actions:
54
+ The root `ApiController` can include any common behavior you want to share across all your API controllers:
52
55
 
53
56
  ```ruby
54
57
  class ApiController < ApplicationController
55
58
  include RESTFramework::BaseControllerMixin
59
+
60
+ # Setting up a paginator class here makes more sense than defining it on every child controller.
61
+ self.paginator_class = RESTFramework::PageNumberPaginator
62
+
63
+ # The page_size attribute doesn't exist on the `BaseControllerMixin`, but for child controllers
64
+ # that include the `ModelControllerMixin`, they will inherit this attribute and will not overwrite
65
+ # it.
66
+ class_attribute(:page_size, default: 30)
67
+ end
68
+ ```
69
+
70
+ A root controller can provide actions that exist on the root of your API.
71
+ It's best to define a dedicated root controller, rather than using the `ApiController` for this purpose, so that actions don't propagate to child controllers:
72
+
73
+ ```ruby
74
+ class Api::RootController < ApiController
56
75
  self.extra_actions = {test: :get}
57
76
 
77
+ def root
78
+ return api_response(
79
+ {
80
+ message: "Welcome to the API.",
81
+ how_to_authenticate: <<~END.lines.map(&:strip).join(" "),
82
+ You can use this API with your normal login session. Otherwise, you can insert your API
83
+ key into a Bearer Authorization header, or into the URL parameters with the name
84
+ `api_key`.
85
+ END
86
+ },
87
+ )
88
+ end
89
+
58
90
  def test
59
- render api_response({message: "Test successful!"})
91
+ return api_response({message: "Hello, world!"})
60
92
  end
61
93
  end
62
94
  ```
63
95
 
64
- `ModelControllerMixin` assists with providing the standard model CRUD for your controller.
96
+ And here is an example of a resource controller:
65
97
 
66
98
  ```ruby
67
99
  class Api::MoviesController < ApiController
68
100
  include RESTFramework::ModelControllerMixin
69
101
 
70
- self.recordset = Movie.where(enabled: true)
102
+ self.fields = [:id, :name, :release_date, :enabled]
103
+ self.extra_member_actions = {first: :get}
104
+
105
+ def first
106
+ # Always use the bang method, since the framework will rescue `RecordNotFound` and return a
107
+ # sensible error response.
108
+ return api_response(self.get_records.first!)
109
+ end
110
+
111
+ def get_recordset
112
+ return Movie.where(enabled: true)
113
+ end
71
114
  end
72
115
  ```
73
116
 
74
- `ReadOnlyModelControllerMixin` only enables list/show actions, but since we're naming this
75
- controller in a way that doesn't make the model obvious, we can set that explicitly:
117
+ When `fields` is nil, then it will default to all columns.
118
+ The `fields` attribute can also be a hash to include or exclude fields rather than defining them manually:
76
119
 
77
120
  ```ruby
78
- class Api::ReadOnlyMoviesController < ApiController
79
- include RESTFramework::ReadOnlyModelControllerMixin
121
+ class Api::UsersController < ApiController
122
+ include RESTFramework::ModelControllerMixin
80
123
 
81
- self.model = Movie
124
+ self.fields = {include: [:calculated_popularity], exclude: [:impersonation_token]}
82
125
  end
83
126
  ```
84
127
 
85
- Note that you can also override the `get_recordset` instance method to override the API behavior
86
- dynamically per-request.
87
-
88
128
  ### Routing
89
129
 
90
- You can use Rails' `resource`/`resources` routers to route your API, however if you want
91
- `extra_actions` / `extra_member_actions` to be routed automatically, then you can use `rest_route`
92
- for non-resourceful controllers, or `rest_resource` / `rest_resources` resourceful routers. You can
93
- also use `rest_root` to route the root of your API:
130
+ Use `rest_route` for non-resourceful controllers, or `rest_resource` / `rest_resources` resourceful routers.
131
+ These routers add some features to the Rails builtin `resource`/`resources` routers, such as automatically routing extra actions defined on the controller.
132
+ To route the root, use `rest_root`.
94
133
 
95
134
  ```ruby
96
135
  Rails.application.routes.draw do
97
- rest_root :api # will find `api_controller` and route the `root` action to '/api'
98
- namespace :api do
99
- rest_resources :movies
100
- rest_resources :users
101
- end
102
- end
103
- ```
104
-
105
- Or if you want the API root to be routed to `Api::RootController#root`:
136
+ # If you wanted to route actions from the `ApiController`, then you would use this:
137
+ # rest_root :api # Will find `api_controller` and route the `root` action to '/api'.
106
138
 
107
- ```ruby
108
- Rails.application.routes.draw do
109
139
  namespace :api do
110
- rest_root # will route `Api::RootController#root` to '/' in this namespace ('/api')
140
+ rest_root # Will route `Api::RootController#root` to '/' in this namespace ('/api').
111
141
  rest_resources :movies
112
142
  rest_resources :users
113
143
  end
@@ -116,15 +146,7 @@ end
116
146
 
117
147
  ## Development/Testing
118
148
 
119
- After you clone the repository, cd'ing into the directory should create a new gemset if you are
120
- using RVM. Then run `bundle install` to install the appropriate gems.
121
-
122
- To run the test suite:
123
-
124
- ```shell
125
- $ rails test
126
- ```
149
+ After you clone the repository, cd'ing into the directory should create a new gemset if you are using RVM.
150
+ Then run `bin/setup` to install the appropriate gems and set things up.
127
151
 
128
- The top-level `bin/rails` proxies all Rails commands to the test project, so you can operate it via
129
- the usual commands. Ensure you run `rails db:setup` before running `rails server` or
130
- `rails console`.
152
+ The top-level `bin/rails` proxies all Rails commands to the test project, so you can operate it via the usual commands (e.g., `rails test`, `rails server` and `rails console`).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.8
1
+ 0.9.10
@@ -29,7 +29,6 @@
29
29
  h1, h2, h3, h4, h5, h6 {
30
30
  color: var(--rrf-red);
31
31
  font-weight: normal;
32
- margin-bottom: 0;
33
32
  }
34
33
  html[data-bs-theme="dark"] h1,
35
34
  html[data-bs-theme="dark"] h2,
@@ -290,7 +289,7 @@
290
289
 
291
290
  // Convert plain-text links to anchor tag links.
292
291
  function rrfLinkify(text) {
293
- return text.replace(/(https?:\/\/[^\s<>"]+)/g, "<a href=\"$1\" target=\"_blank\">$1</a>")
292
+ return text.replace(/(https?:\/\/[^\s<>"]+)/g, "<a href=\"$1\">$1</a>")
294
293
  }
295
294
 
296
295
  // Replace the document when doing form submission (mainly to support PUT/PATCH/DELETE).
@@ -1,7 +1,7 @@
1
1
  <div class="row">
2
2
  <div>
3
3
  <%= render partial: "rest_framework/heading/actions" if @route_groups.present? %>
4
- <h1 class="m-0"><%= @heading_title || @title %></h1>
4
+ <h1 style="margin: 0"><%= @heading_title || @title %></h1>
5
5
  <% if @description.present? %>
6
6
  <br><br><p style="display: inline-block; margin-bottom: 0"><%= @description %></p>
7
7
  <% end %>
@@ -17,7 +17,7 @@
17
17
  <% end %>
18
18
  </ul>
19
19
  </div>
20
- <div class="tab-content pt-2">
20
+ <div class="tab-content">
21
21
  <div class="tab-pane fade show active" id="tab-json" role="tabpanel">
22
22
  <% if @json_payload.present? %>
23
23
  <div><pre class="rrf-copy"><code class="language-json"><%=
@@ -1,16 +1,14 @@
1
1
  <div class="row">
2
2
  <div>
3
- <pre><code><%
3
+ <pre class="mb-2"><code class="language-plaintext"><%
4
4
  concat request.request_method
5
5
  if request.method != request.request_method
6
6
  concat " (via #{request.method})"
7
7
  end
8
8
  concat " #{request.path}"
9
9
  %></code></pre>
10
- <pre><code><%
10
+ <pre><code class="language-plaintext"><%
11
11
  concat "HTTP #{response.status} #{response.message}"
12
- concat "\n"
13
- concat "Content-Type: #{response.content_type}"
14
12
  %></code></pre>
15
13
  </div>
16
14
  </div>
@@ -34,7 +34,7 @@
34
34
  <% end %>
35
35
  </ul>
36
36
  </div>
37
- <div class="tab-content pt-2">
37
+ <div class="tab-content">
38
38
  <div class="tab-pane fade show active" id="tab-routes" role="tabpanel">
39
39
  <%= render partial: "rest_framework/routes_and_forms/routes" %>
40
40
  </div>
@@ -49,4 +49,4 @@
49
49
  </div>
50
50
  <% end %>
51
51
  </div>
52
- </div>
52
+ </div>
@@ -0,0 +1,5 @@
1
+ class RESTFramework::Errors::BaseError < StandardError
2
+ end
3
+
4
+ # Alias for convenience.
5
+ RESTFramework::BaseError = RESTFramework::Errors::BaseError
@@ -0,0 +1,14 @@
1
+ class RESTFramework::Errors::NilPassedToAPIResponseError < RESTFramework::Errors::BaseError
2
+ def message
3
+ return <<~MSG.split("\n").join(" ")
4
+ Payload of `nil` was passed to `api_response`; 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::NilPassedToAPIResponseError = RESTFramework::Errors::NilPassedToAPIResponseError
@@ -0,0 +1,18 @@
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
+ return <<~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,31 +1,7 @@
1
- # Top-level class for all REST Framework errors.
2
- class RESTFramework::Error < StandardError
1
+ module RESTFramework::Errors
3
2
  end
4
3
 
5
- class RESTFramework::NilPassedToAPIResponseError < RESTFramework::Error
6
- def message
7
- return <<~MSG.split("\n").join(" ")
8
- Payload of `nil` was passed to `api_response`; this is unsupported. If you want a blank
9
- response, pass `''` (an empty string) as the payload. If this was the result of a `find_by`
10
- (or similar Active Record method) not finding a record, you should use the bang version (e.g.,
11
- `find_by!`) to raise `ActiveRecord::RecordNotFound`, which the REST controller will catch and
12
- return an appropriate error response.
13
- MSG
14
- end
15
- end
16
-
17
- class RESTFramework::UnknownModelError < RESTFramework::Error
18
- def initialize(controller_class)
19
- super()
20
- @controller_class = controller_class
21
- end
4
+ require_relative "errors/base_error"
22
5
 
23
- def message
24
- return <<~MSG.split("\n").join(" ")
25
- The model class for `#{@controller_class}` could not be determined. Any controller that
26
- includes `RESTFramework::BaseModelControllerMixin` (directly or indirectly) must either set
27
- the `model` attribute on the controller, or the model must be deducible from the controller
28
- name (e.g., `UsersController` could resolve to the `User` model).
29
- MSG
30
- end
31
- end
6
+ require_relative "errors/nil_passed_to_api_response_error"
7
+ require_relative "errors/unknown_model_error"
@@ -15,6 +15,7 @@ class RESTFramework::Filters::ModelOrderingFilter < RESTFramework::Filters::Base
15
15
 
16
16
  if order_string.present?
17
17
  ordering = {}.with_indifferent_access
18
+
18
19
  order_string.split(",").each do |field|
19
20
  if field[0] == "-"
20
21
  column = field[1..-1]
@@ -24,10 +25,11 @@ class RESTFramework::Filters::ModelOrderingFilter < RESTFramework::Filters::Base
24
25
  direction = :asc
25
26
  end
26
27
 
27
- next if !column.in?(fields) && column.split(".").first.in?(fields)
28
+ next if !column.in?(fields) && !column.split(".").first.in?(fields)
28
29
 
29
30
  ordering[column] = direction
30
31
  end
32
+
31
33
  return ordering
32
34
  end
33
35
 
@@ -1,5 +1,7 @@
1
1
  # A simple filtering backend that supports filtering a recordset based on query parameters.
2
2
  class RESTFramework::Filters::ModelQueryFilter < RESTFramework::Filters::BaseFilter
3
+ NIL_VALUES = ["nil", "null"].freeze
4
+
3
5
  # Get a list of filterset fields for the current action.
4
6
  def _get_fields
5
7
  # Always return a list of strings; `@controller.get_fields` already does this.
@@ -9,8 +11,9 @@ class RESTFramework::Filters::ModelQueryFilter < RESTFramework::Filters::BaseFil
9
11
  # Filter params for keys allowed by the current action's filterset_fields/fields config.
10
12
  def _get_filter_params
11
13
  fields = self._get_fields
14
+ includes = []
12
15
 
13
- return @controller.request.query_parameters.select { |p, _|
16
+ filter_params = @controller.request.query_parameters.select { |p, _|
14
17
  # Remove any trailing `__in` from the field name.
15
18
  field = p.chomp("__in")
16
19
 
@@ -20,7 +23,12 @@ class RESTFramework::Filters::ModelQueryFilter < RESTFramework::Filters::BaseFil
20
23
  next false unless field.in?(fields)
21
24
 
22
25
  sub_fields = @controller.class.get_field_config(field)[:sub_fields] || []
23
- next sub_field.in?(sub_fields)
26
+ if sub_field.in?(sub_fields)
27
+ includes << field.to_sym
28
+ next true
29
+ end
30
+
31
+ next false
24
32
  end
25
33
 
26
34
  next field.in?(fields)
@@ -28,21 +36,27 @@ class RESTFramework::Filters::ModelQueryFilter < RESTFramework::Filters::BaseFil
28
36
  # Convert fields ending in `__in` to array values.
29
37
  if p.end_with?("__in")
30
38
  p = p.chomp("__in")
31
- v = v.split(",")
39
+ v = v.split(",").map { |v| v.in?(NIL_VALUES) ? nil : v }
32
40
  end
33
41
 
34
42
  # Convert "nil" and "null" to nil.
35
- if v == "nil" || v == "null"
36
- v = nil
37
- end
43
+ v = nil if v.in?(NIL_VALUES)
38
44
 
39
45
  [p, v]
40
46
  }.to_h.symbolize_keys
47
+
48
+ return filter_params, includes
41
49
  end
42
50
 
43
51
  # Filter data according to the request query parameters.
44
52
  def get_filtered_data(data)
45
- if filter_params = self._get_filter_params.presence
53
+ filter_params, includes = self._get_filter_params
54
+
55
+ if filter_params.any?
56
+ if includes.any?
57
+ data = data.includes(*includes)
58
+ end
59
+
46
60
  return data.where(**filter_params)
47
61
  end
48
62
 
@@ -31,9 +31,6 @@ module RESTFramework::Mixins::BulkCreateModelMixin
31
31
  end
32
32
  end
33
33
 
34
- # Alias for convenience.
35
- RESTFramework::BulkCreateModelMixin = RESTFramework::Mixins::BulkCreateModelMixin
36
-
37
34
  # Mixin for updating records in bulk.
38
35
  module RESTFramework::Mixins::BulkUpdateModelMixin
39
36
  def update_all
@@ -59,9 +56,6 @@ module RESTFramework::Mixins::BulkUpdateModelMixin
59
56
  end
60
57
  end
61
58
 
62
- # Alias for convenience.
63
- RESTFramework::BulkUpdateModelMixin = RESTFramework::Mixins::BulkUpdateModelMixin
64
-
65
59
  # Mixin for destroying records in bulk.
66
60
  module RESTFramework::Mixins::BulkDestroyModelMixin
67
61
  def destroy_all
@@ -89,21 +83,21 @@ module RESTFramework::Mixins::BulkDestroyModelMixin
89
83
  end
90
84
  end
91
85
 
92
- # Alias for convenience.
93
- RESTFramework::BulkDestroyModelMixin = RESTFramework::Mixins::BulkDestroyModelMixin
94
-
95
86
  # Mixin that includes all the CRUD bulk mixins.
96
87
  module RESTFramework::Mixins::BulkModelControllerMixin
97
- include RESTFramework::ModelControllerMixin
88
+ include RESTFramework::Mixins::ModelControllerMixin
98
89
 
99
- include RESTFramework::BulkCreateModelMixin
100
- include RESTFramework::BulkUpdateModelMixin
101
- include RESTFramework::BulkDestroyModelMixin
90
+ include RESTFramework::Mixins::BulkCreateModelMixin
91
+ include RESTFramework::Mixins::BulkUpdateModelMixin
92
+ include RESTFramework::Mixins::BulkDestroyModelMixin
102
93
 
103
94
  def self.included(base)
104
- RESTFramework::ModelControllerMixin.included(base)
95
+ RESTFramework::Mixins::ModelControllerMixin.included(base)
105
96
  end
106
97
  end
107
98
 
108
- # Alias for convenience.
99
+ # Aliases for convenience.
100
+ RESTFramework::BulkCreateModelMixin = RESTFramework::Mixins::BulkCreateModelMixin
101
+ RESTFramework::BulkUpdateModelMixin = RESTFramework::Mixins::BulkUpdateModelMixin
102
+ RESTFramework::BulkDestroyModelMixin = RESTFramework::Mixins::BulkDestroyModelMixin
109
103
  RESTFramework::BulkModelControllerMixin = RESTFramework::Mixins::BulkModelControllerMixin
@@ -33,17 +33,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
33
33
  # Options for handling request body parameters.
34
34
  allowed_parameters: nil,
35
35
  filter_pk_from_request_body: true,
36
- exclude_body_fields: %w[
37
- created_at
38
- created_by
39
- created_by_id
40
- updated_at
41
- updated_by
42
- updated_by_id
43
- _method
44
- utf8
45
- authenticity_token
46
- ].freeze,
36
+ exclude_body_fields: RESTFramework.config.exclude_body_fields,
47
37
 
48
38
  # Attributes for the default native serializer.
49
39
  native_serializer_config: nil,
@@ -515,7 +505,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
515
505
  ActionController::Parameters.new(data).permit(*allowed_params)
516
506
  end
517
507
 
518
- # Filter primary key if configured.
508
+ # Filter primary key, if configured.
519
509
  if self.filter_pk_from_request_body && bulk_mode != :update
520
510
  body_params.delete(pk)
521
511
  end
@@ -527,7 +517,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
527
517
  #
528
518
  # rubocop:disable Layout/LineLength
529
519
  #
530
- # Good example base64 image:
520
+ # Example base64 image:
531
521
  # data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=
532
522
  #
533
523
  # rubocop:enable Layout/LineLength
@@ -572,7 +562,6 @@ module RESTFramework::Mixins::BaseModelControllerMixin
572
562
  # Cache the result.
573
563
  return @record if @record
574
564
 
575
- recordset = self.get_recordset
576
565
  find_by_key = self.class.get_model.primary_key
577
566
  is_pk = true
578
567
 
@@ -588,16 +577,18 @@ module RESTFramework::Mixins::BaseModelControllerMixin
588
577
  end
589
578
  end
590
579
 
591
- # Filter recordset, if configured.
592
- if self.filter_recordset_before_find
593
- recordset = self.get_records
580
+ # Get the recordset, filtering if configured.
581
+ collection = if self.filter_recordset_before_find
582
+ self.get_records
583
+ else
584
+ self.get_recordset
594
585
  end
595
586
 
596
587
  # Return the record. Route key is always `:id` by Rails convention.
597
588
  if is_pk
598
- return @record = recordset.find(request.path_parameters[:id])
589
+ return @record = collection.find(request.path_parameters[:id])
599
590
  else
600
- return @record = recordset.find_by!(find_by_key => request.path_parameters[:id])
591
+ return @record = collection.find_by!(find_by_key => request.path_parameters[:id])
601
592
  end
602
593
  end
603
594
 
@@ -631,9 +622,6 @@ module RESTFramework::Mixins::BaseModelControllerMixin
631
622
  end
632
623
  end
633
624
 
634
- # Alias for convenience.
635
- RESTFramework::BaseModelControllerMixin = RESTFramework::Mixins::BaseModelControllerMixin
636
-
637
625
  # Mixin for listing records.
638
626
  module RESTFramework::Mixins::ListModelMixin
639
627
  def index
@@ -662,9 +650,6 @@ module RESTFramework::Mixins::ListModelMixin
662
650
  end
663
651
  end
664
652
 
665
- # Alias for convenience.
666
- RESTFramework::ListModelMixin = RESTFramework::Mixins::ListModelMixin
667
-
668
653
  # Mixin for showing records.
669
654
  module RESTFramework::Mixins::ShowModelMixin
670
655
  def show
@@ -672,9 +657,6 @@ module RESTFramework::Mixins::ShowModelMixin
672
657
  end
673
658
  end
674
659
 
675
- # Alias for convenience.
676
- RESTFramework::ShowModelMixin = RESTFramework::Mixins::ShowModelMixin
677
-
678
660
  # Mixin for creating records.
679
661
  module RESTFramework::Mixins::CreateModelMixin
680
662
  def create
@@ -687,9 +669,6 @@ module RESTFramework::Mixins::CreateModelMixin
687
669
  end
688
670
  end
689
671
 
690
- # Alias for convenience.
691
- RESTFramework::CreateModelMixin = RESTFramework::Mixins::CreateModelMixin
692
-
693
672
  # Mixin for updating records.
694
673
  module RESTFramework::Mixins::UpdateModelMixin
695
674
  def update
@@ -704,9 +683,6 @@ module RESTFramework::Mixins::UpdateModelMixin
704
683
  end
705
684
  end
706
685
 
707
- # Alias for convenience.
708
- RESTFramework::UpdateModelMixin = RESTFramework::Mixins::UpdateModelMixin
709
-
710
686
  # Mixin for destroying records.
711
687
  module RESTFramework::Mixins::DestroyModelMixin
712
688
  def destroy
@@ -720,38 +696,39 @@ module RESTFramework::Mixins::DestroyModelMixin
720
696
  end
721
697
  end
722
698
 
723
- # Alias for convenience.
724
- RESTFramework::DestroyModelMixin = RESTFramework::Mixins::DestroyModelMixin
725
-
726
699
  # Mixin that includes show/list mixins.
727
700
  module RESTFramework::Mixins::ReadOnlyModelControllerMixin
728
- include RESTFramework::BaseModelControllerMixin
701
+ include RESTFramework::Mixins::BaseModelControllerMixin
729
702
 
730
- include RESTFramework::ListModelMixin
731
- include RESTFramework::ShowModelMixin
703
+ include RESTFramework::Mixins::ListModelMixin
704
+ include RESTFramework::Mixins::ShowModelMixin
732
705
 
733
706
  def self.included(base)
734
707
  RESTFramework::BaseModelControllerMixin.included(base)
735
708
  end
736
709
  end
737
710
 
738
- # Alias for convenience.
739
- RESTFramework::ReadOnlyModelControllerMixin = RESTFramework::Mixins::ReadOnlyModelControllerMixin
740
-
741
711
  # Mixin that includes all the CRUD mixins.
742
712
  module RESTFramework::Mixins::ModelControllerMixin
743
- include RESTFramework::BaseModelControllerMixin
713
+ include RESTFramework::Mixins::BaseModelControllerMixin
744
714
 
745
- include RESTFramework::ListModelMixin
746
- include RESTFramework::ShowModelMixin
747
- include RESTFramework::CreateModelMixin
748
- include RESTFramework::UpdateModelMixin
749
- include RESTFramework::DestroyModelMixin
715
+ include RESTFramework::Mixins::ListModelMixin
716
+ include RESTFramework::Mixins::ShowModelMixin
717
+ include RESTFramework::Mixins::CreateModelMixin
718
+ include RESTFramework::Mixins::UpdateModelMixin
719
+ include RESTFramework::Mixins::DestroyModelMixin
750
720
 
751
721
  def self.included(base)
752
722
  RESTFramework::BaseModelControllerMixin.included(base)
753
723
  end
754
724
  end
755
725
 
756
- # Alias for convenience.
726
+ # Aliases for convenience.
727
+ RESTFramework::BaseModelControllerMixin = RESTFramework::Mixins::BaseModelControllerMixin
728
+ RESTFramework::ListModelMixin = RESTFramework::Mixins::ListModelMixin
729
+ RESTFramework::ShowModelMixin = RESTFramework::Mixins::ShowModelMixin
730
+ RESTFramework::CreateModelMixin = RESTFramework::Mixins::CreateModelMixin
731
+ RESTFramework::UpdateModelMixin = RESTFramework::Mixins::UpdateModelMixin
732
+ RESTFramework::DestroyModelMixin = RESTFramework::Mixins::DestroyModelMixin
733
+ RESTFramework::ReadOnlyModelControllerMixin = RESTFramework::Mixins::ReadOnlyModelControllerMixin
757
734
  RESTFramework::ModelControllerMixin = RESTFramework::Mixins::ModelControllerMixin
@@ -59,13 +59,24 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
59
59
 
60
60
  # Wrap the serialized page with appropriate metadata. TODO: include links.
61
61
  def get_paginated_response(serialized_page)
62
+ page_query_param = @controller.page_query_param
63
+ base_params = @controller.params.to_unsafe_h
64
+ next_url = if @page_number < @total_pages
65
+ @controller.url_for({**base_params, page_query_param => @page_number + 1})
66
+ end
67
+ previous_url = if @page_number > 1
68
+ @controller.url_for({**base_params, page_query_param => @page_number - 1})
69
+ end
70
+
62
71
  return {
63
72
  count: @count,
64
73
  page: @page_number,
65
74
  page_size: @page_size,
66
75
  total_pages: @total_pages,
76
+ next: next_url,
77
+ previous: previous_url,
67
78
  results: serialized_page,
68
- }
79
+ }.compact
69
80
  end
70
81
  end
71
82
 
@@ -1,5 +1,5 @@
1
- # Do not use Rails-specific helper methods here (e.g., `blank?`) so the module can run standalone.
2
1
  module RESTFramework
2
+ # Do not use Rails-specific helper methods here (e.g., `blank?`) so the module can run standalone.
3
3
  module Version
4
4
  VERSION_FILEPATH = File.expand_path("../../VERSION", __dir__)
5
5
  UNKNOWN = "0-unknown"
@@ -122,9 +122,19 @@ module RESTFramework
122
122
  # Global configuration should be kept minimal, as controller-level configurations allows multiple
123
123
  # APIs to be defined to behave differently.
124
124
  class Config
125
- DEFAULT_EXCLUDE_ASSOCIATION_CLASSES = [].freeze
126
125
  DEFAULT_LABEL_FIELDS = %w(name label login title email username url).freeze
127
126
  DEFAULT_SEARCH_COLUMNS = DEFAULT_LABEL_FIELDS + %w(description note).freeze
127
+ DEFAULT_EXCLUDE_BODY_FIELDS = %w[
128
+ created_at
129
+ created_by
130
+ created_by_id
131
+ updated_at
132
+ updated_by
133
+ updated_by_id
134
+ _method
135
+ utf8
136
+ authenticity_token
137
+ ].freeze
128
138
 
129
139
  # Do not run `rrf_finalize` on controllers automatically using a `TracePoint` hook. This is a
130
140
  # performance option and must be global because we have to determine this before any
@@ -155,6 +165,9 @@ module RESTFramework
155
165
  # The default search columns to use when generating search filters.
156
166
  attr_accessor :search_columns
157
167
 
168
+ # The default list of fields to exclude from the body of the request.
169
+ attr_accessor :exclude_body_fields
170
+
158
171
  # Option to use vendored assets (requires sprockets or propshaft) rather than linking to
159
172
  # external assets (the default).
160
173
  attr_accessor :use_vendored_assets
@@ -163,6 +176,7 @@ module RESTFramework
163
176
  self.show_backtrace = Rails.env.development?
164
177
  self.label_fields = DEFAULT_LABEL_FIELDS
165
178
  self.search_columns = DEFAULT_SEARCH_COLUMNS
179
+ self.exclude_body_fields = DEFAULT_EXCLUDE_BODY_FIELDS
166
180
  end
167
181
  end
168
182
 
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: 0.9.8
4
+ version: 0.9.10
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: 2023-07-07 00:00:00.000000000 Z
11
+ date: 2023-07-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -53,6 +53,9 @@ files:
53
53
  - lib/rest_framework.rb
54
54
  - lib/rest_framework/engine.rb
55
55
  - lib/rest_framework/errors.rb
56
+ - lib/rest_framework/errors/base_error.rb
57
+ - lib/rest_framework/errors/nil_passed_to_api_response_error.rb
58
+ - lib/rest_framework/errors/unknown_model_error.rb
56
59
  - lib/rest_framework/filters.rb
57
60
  - lib/rest_framework/filters/base_filter.rb
58
61
  - lib/rest_framework/filters/model_ordering_filter.rb