rest_framework 0.9.8 → 0.9.10

Sign up to get free protection for your applications and to get access to all the features.
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