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 +4 -4
- data/README.md +76 -54
- data/VERSION +1 -1
- data/app/views/rest_framework/_head.html.erb +1 -2
- data/app/views/rest_framework/_heading.html.erb +1 -1
- data/app/views/rest_framework/_payloads.html.erb +1 -1
- data/app/views/rest_framework/_request_metadata.html.erb +2 -4
- data/app/views/rest_framework/_routes_and_forms.html.erb +2 -2
- data/lib/rest_framework/errors/base_error.rb +5 -0
- data/lib/rest_framework/errors/nil_passed_to_api_response_error.rb +14 -0
- data/lib/rest_framework/errors/unknown_model_error.rb +18 -0
- data/lib/rest_framework/errors.rb +4 -28
- data/lib/rest_framework/filters/model_ordering_filter.rb +3 -1
- data/lib/rest_framework/filters/model_query_filter.rb +21 -7
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +9 -15
- data/lib/rest_framework/mixins/model_controller_mixin.rb +27 -50
- data/lib/rest_framework/paginators/page_number_paginator.rb +12 -1
- data/lib/rest_framework/version.rb +1 -1
- data/lib/rest_framework.rb +15 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2db255f618d449ef621f1337d182993c21b5d8d87e5e8c960c8b42093248c91d
|
4
|
+
data.tar.gz: 4612f2e11733ae69e418cd2877b8150e2a1dbd4fb0cd34ed29727dcec007f305
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
28
|
+
gem "rest_framework"
|
31
29
|
```
|
32
30
|
|
33
|
-
And then
|
31
|
+
And then run:
|
34
32
|
|
35
33
|
```shell
|
36
|
-
|
34
|
+
bundle install
|
37
35
|
```
|
38
36
|
|
39
|
-
|
37
|
+
## Quick Usage Tutorial
|
40
38
|
|
41
|
-
|
42
|
-
$ gem install rest_framework
|
43
|
-
```
|
39
|
+
This section provides some simple examples to quickly get you started using the framework.
|
44
40
|
|
45
|
-
|
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
|
-
|
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
|
-
|
91
|
+
return api_response({message: "Hello, world!"})
|
60
92
|
end
|
61
93
|
end
|
62
94
|
```
|
63
95
|
|
64
|
-
|
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.
|
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
|
-
`
|
75
|
-
|
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::
|
79
|
-
include RESTFramework::
|
121
|
+
class Api::UsersController < ApiController
|
122
|
+
include RESTFramework::ModelControllerMixin
|
80
123
|
|
81
|
-
self.
|
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
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
98
|
-
|
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 #
|
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
|
-
|
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.
|
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\"
|
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
|
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
|
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
|
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,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
|
-
|
2
|
-
class RESTFramework::Error < StandardError
|
1
|
+
module RESTFramework::Errors
|
3
2
|
end
|
4
3
|
|
5
|
-
|
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
|
-
|
24
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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:
|
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
|
-
#
|
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
|
-
#
|
592
|
-
if self.filter_recordset_before_find
|
593
|
-
|
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 =
|
589
|
+
return @record = collection.find(request.path_parameters[:id])
|
599
590
|
else
|
600
|
-
return @record =
|
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
|
-
#
|
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"
|
data/lib/rest_framework.rb
CHANGED
@@ -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.
|
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-
|
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
|