rest_framework 0.0.15 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +17 -23
- data/app/views/layouts/rest_framework.html.erb +2 -2
- data/lib/rest_framework.rb +6 -0
- data/lib/rest_framework/VERSION_STAMP +1 -1
- data/lib/rest_framework/controller_mixins.rb +4 -0
- data/lib/rest_framework/controller_mixins/base.rb +166 -111
- data/lib/rest_framework/controller_mixins/models.rb +216 -178
- data/lib/rest_framework/errors.rb +26 -0
- data/lib/rest_framework/filters.rb +68 -0
- data/lib/rest_framework/generators.rb +5 -0
- data/lib/rest_framework/generators/controller_generator.rb +26 -0
- data/lib/rest_framework/paginators.rb +95 -0
- data/lib/rest_framework/routers.rb +88 -44
- data/lib/rest_framework/serializers.rb +132 -81
- data/lib/rest_framework/version.rb +11 -9
- metadata +10 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8ef7499f4d1e10af1f2520d3b9847dc7f711bd1c7f4377efe21bd5b60c0dd9a7
|
4
|
+
data.tar.gz: b15a9b0abee58e315fdc3a1157ca04d7051630821d3ed2ca691564966b5fefca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb586a083989a67f206d5b5d6c14eb546949569e384d3b44c870de5f49f610b5848d53da87e01234542705304dd7d49bcf6f0037f0b89331c7fa2a1b084422ce
|
7
|
+
data.tar.gz: fe4c960cda0d2a9ba43cb7d5b1cfa84963802800829323c26b350841bbd34290e68cbbc4f14a3138a1cb95a28aafff76b2d9a78a41772950e00ad80159c8f2f3
|
data/README.md
CHANGED
@@ -2,16 +2,22 @@
|
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/rest_framework)
|
4
4
|
[](https://travis-ci.org/gregschmit/rails-rest-framework)
|
5
|
+
[](https://coveralls.io/github/gregschmit/rails-rest-framework?branch=master)
|
5
6
|
|
6
|
-
|
7
|
+
A framework for DRY RESTful APIs in Ruby on Rails.
|
7
8
|
|
8
9
|
**The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD
|
9
|
-
logic, and routing them can be obnoxious.
|
10
|
+
logic, and routing them can be obnoxious. Building and maintaining features like ordering,
|
11
|
+
filtering, and pagination can be tedious.
|
10
12
|
|
11
|
-
**The Solution**: This
|
12
|
-
|
13
|
+
**The Solution**: This framework implements browsable API responses, CRUD actions for your models,
|
14
|
+
and features like ordering/filtering/pagination, so you can focus on building awesome APIs.
|
13
15
|
|
14
|
-
|
16
|
+
Website/Guide: [https://rails-rest-framework.com](https://rails-rest-framework.com)
|
17
|
+
|
18
|
+
Source: [https://github.com/gregschmit/rails-rest-framework](https://github.com/gregschmit/rails-rest-framework)
|
19
|
+
|
20
|
+
YARD Docs: [https://rubydoc.info/gems/rest_framework](https://rubydoc.info/gems/rest_framework)
|
15
21
|
|
16
22
|
## Installation
|
17
23
|
|
@@ -73,8 +79,8 @@ class Api::ReadOnlyMoviesController < ApiController
|
|
73
79
|
end
|
74
80
|
```
|
75
81
|
|
76
|
-
Note that you can also override
|
77
|
-
|
82
|
+
Note that you can also override the `get_recordset` instance method to override the API behavior
|
83
|
+
dynamically per-request.
|
78
84
|
|
79
85
|
### Routing
|
80
86
|
|
@@ -110,24 +116,12 @@ end
|
|
110
116
|
After you clone the repository, cd'ing into the directory should create a new gemset if you are
|
111
117
|
using RVM. Then run `bundle install` to install the appropriate gems.
|
112
118
|
|
113
|
-
To run the
|
119
|
+
To run the test suite:
|
114
120
|
|
115
121
|
```shell
|
116
122
|
$ rake test
|
117
123
|
```
|
118
124
|
|
119
|
-
To
|
120
|
-
|
121
|
-
|
122
|
-
$ rake test:unit
|
123
|
-
```
|
124
|
-
|
125
|
-
To run integration tests:
|
126
|
-
|
127
|
-
```shell
|
128
|
-
$ rake test:integration
|
129
|
-
```
|
130
|
-
|
131
|
-
To interact with the integration app, you can `cd test/integration` and operate it via the normal
|
132
|
-
Rails interfaces. Ensure you run `rake db:schema:load` before running `rails server` or
|
133
|
-
`rails console`.
|
125
|
+
To interact with the test app, `cd test` and operate it via the normal Rails interfaces. Ensure you
|
126
|
+
run `rake db:schema:load` before running `rails server` or `rails console`. You can also load the
|
127
|
+
test fixtures with `rake db:fixtures:load`.
|
@@ -30,14 +30,14 @@
|
|
30
30
|
<% if @json_payload %>
|
31
31
|
<li class="nav-item">
|
32
32
|
<a class="nav-link active" href="#tab-json" data-toggle="tab" role="tab">
|
33
|
-
|
33
|
+
.json
|
34
34
|
</a>
|
35
35
|
</li>
|
36
36
|
<% end %>
|
37
37
|
<% if @xml_payload %>
|
38
38
|
<li class="nav-item">
|
39
39
|
<a class="nav-link" href="#tab-xml" data-toggle="tab" role="tab">
|
40
|
-
|
40
|
+
.xml
|
41
41
|
</a>
|
42
42
|
</li>
|
43
43
|
<% end %>
|
data/lib/rest_framework.rb
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
module RESTFramework
|
2
2
|
end
|
3
3
|
|
4
|
+
|
4
5
|
require_relative "rest_framework/controller_mixins"
|
5
6
|
require_relative "rest_framework/engine"
|
7
|
+
require_relative "rest_framework/errors"
|
8
|
+
require_relative "rest_framework/filters"
|
9
|
+
require_relative "rest_framework/paginators"
|
6
10
|
require_relative "rest_framework/routers"
|
11
|
+
require_relative "rest_framework/serializers"
|
7
12
|
require_relative "rest_framework/version"
|
13
|
+
require_relative "rest_framework/generators"
|
@@ -1 +1 @@
|
|
1
|
-
0.0
|
1
|
+
0.2.0
|
@@ -1,134 +1,189 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
# This module provides the common functionality for any controller mixins, a `root` action, and
|
4
|
-
# the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
|
5
|
-
# is defined.
|
6
|
-
module BaseControllerMixin
|
7
|
-
# Default action for API root.
|
8
|
-
def root
|
9
|
-
api_response({message: "This is the root of your awesome API!"})
|
10
|
-
end
|
1
|
+
require_relative '../errors'
|
2
|
+
require_relative '../serializers'
|
11
3
|
|
12
|
-
module ClassMethods
|
13
|
-
def get_skip_actions(skip_undefined: true)
|
14
|
-
# first, skip explicitly skipped actions
|
15
|
-
skip = self.skip_actions || []
|
16
4
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
5
|
+
# This module provides the common functionality for any controller mixins, a `root` action, and
|
6
|
+
# the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
|
7
|
+
# is defined.
|
8
|
+
module RESTFramework::BaseControllerMixin
|
9
|
+
# Default action for API root.
|
10
|
+
def root
|
11
|
+
api_response({message: "This is the root of your awesome API!"})
|
12
|
+
end
|
23
13
|
|
24
|
-
|
14
|
+
module ClassMethods
|
15
|
+
# Helper to get the actions that should be skipped.
|
16
|
+
def get_skip_actions(skip_undefined: true)
|
17
|
+
# First, skip explicitly skipped actions.
|
18
|
+
skip = self.skip_actions || []
|
19
|
+
|
20
|
+
# Now add methods which don't exist, since we don't want to route those.
|
21
|
+
if skip_undefined
|
22
|
+
[:index, :new, :create, :show, :edit, :update, :destroy].each do |a|
|
23
|
+
skip << a unless self.method_defined?(a)
|
24
|
+
end
|
25
25
|
end
|
26
|
+
|
27
|
+
return skip
|
26
28
|
end
|
29
|
+
end
|
27
30
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
31
|
+
def self.included(base)
|
32
|
+
if base.is_a? Class
|
33
|
+
base.extend ClassMethods
|
34
|
+
|
35
|
+
# Add class attributes (with defaults) unless they already exist.
|
36
|
+
{
|
37
|
+
filter_pk_from_request_body: true,
|
38
|
+
exclude_body_fields: [:created_at, :created_by, :updated_at, :updated_by],
|
39
|
+
accept_generic_params_as_body_params: true,
|
40
|
+
extra_actions: nil,
|
41
|
+
extra_member_actions: nil,
|
42
|
+
filter_backends: nil,
|
43
|
+
paginator_class: nil,
|
44
|
+
page_size: 20,
|
45
|
+
page_query_param: 'page',
|
46
|
+
page_size_query_param: 'page_size',
|
47
|
+
max_page_size: nil,
|
48
|
+
serializer_class: nil,
|
49
|
+
serialize_to_json: true,
|
50
|
+
serialize_to_xml: true,
|
51
|
+
singleton_controller: nil,
|
52
|
+
skip_actions: nil,
|
53
|
+
}.each do |a, default|
|
54
|
+
unless base.respond_to?(a)
|
55
|
+
base.class_attribute(a)
|
56
|
+
|
57
|
+
# Set default manually so we can still support Rails 4. Maybe later we can use the default
|
58
|
+
# parameter on `class_attribute`.
|
59
|
+
base.send(:"#{a}=", default)
|
60
|
+
end
|
46
61
|
end
|
47
|
-
end
|
48
62
|
|
49
|
-
|
63
|
+
# Alias `extra_actions` to `extra_collection_actions`.
|
64
|
+
unless base.respond_to?(:extra_collection_actions)
|
65
|
+
base.alias_method(:extra_collection_actions, :extra_actions)
|
66
|
+
base.alias_method(:extra_collection_actions=, :extra_actions=)
|
67
|
+
end
|
50
68
|
|
51
|
-
|
52
|
-
|
53
|
-
{message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
|
54
|
-
)
|
55
|
-
end
|
69
|
+
# Skip csrf since this is an API.
|
70
|
+
base.skip_before_action(:verify_authenticity_token) rescue nil
|
56
71
|
|
57
|
-
|
58
|
-
|
72
|
+
# Handle some common exceptions.
|
73
|
+
base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
|
74
|
+
base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
|
75
|
+
base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
|
76
|
+
base.rescue_from(ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed)
|
59
77
|
end
|
78
|
+
end
|
60
79
|
|
61
|
-
|
62
|
-
return api_response({message: "Record not saved.", exception: e}, status: 406)
|
63
|
-
end
|
80
|
+
protected
|
64
81
|
|
65
|
-
|
66
|
-
|
67
|
-
|
82
|
+
# Helper to get filtering backends with a sane default.
|
83
|
+
# @return [RESTFramework::BaseFilter]
|
84
|
+
def get_filter_backends
|
85
|
+
return self.class.filter_backends || []
|
86
|
+
end
|
68
87
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
end
|
75
|
-
return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
|
76
|
-
formatter.new
|
77
|
-
).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
|
78
|
-
{verb: r[0], path: r[1], action: r[2]}
|
79
|
-
}.select { |r| r[:path].start_with?(request.path) }
|
88
|
+
# Helper to filter an arbitrary data set over all configured filter backends.
|
89
|
+
def get_filtered_data(data)
|
90
|
+
self.get_filter_backends.each do |filter_class|
|
91
|
+
filter = filter_class.new(controller: self)
|
92
|
+
data = filter.get_filtered_data(data)
|
80
93
|
end
|
81
94
|
|
82
|
-
|
83
|
-
|
84
|
-
def api_response(payload, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
|
85
|
-
html_kwargs ||= {}
|
86
|
-
json_kwargs ||= {}
|
87
|
-
xml_kwargs ||= {}
|
95
|
+
return data
|
96
|
+
end
|
88
97
|
|
89
|
-
|
90
|
-
|
98
|
+
# Helper to get the configured serializer class.
|
99
|
+
# @return [RESTFramework::BaseSerializer]
|
100
|
+
def get_serializer_class
|
101
|
+
return self.class.serializer_class
|
102
|
+
end
|
91
103
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
104
|
+
def record_invalid(e)
|
105
|
+
return api_response(
|
106
|
+
{message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
def record_not_found(e)
|
111
|
+
return api_response({message: "Record not found.", exception: e}, status: 404)
|
112
|
+
end
|
113
|
+
|
114
|
+
def record_not_saved(e)
|
115
|
+
return api_response({message: "Record not saved.", exception: e}, status: 406)
|
116
|
+
end
|
117
|
+
|
118
|
+
def record_not_destroyed(e)
|
119
|
+
return api_response({message: "Record not destroyed.", exception: e}, status: 406)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Helper for showing routes under a controller action, used for the browsable API.
|
123
|
+
def _get_routes
|
124
|
+
begin
|
125
|
+
formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
|
126
|
+
rescue NameError
|
127
|
+
formatter = ActionDispatch::Routing::ConsoleFormatter
|
128
|
+
end
|
129
|
+
return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
|
130
|
+
formatter.new
|
131
|
+
).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
|
132
|
+
{verb: r[0], path: r[1], action: r[2]}
|
133
|
+
}.select { |r| r[:path].start_with?(request.path) }
|
134
|
+
end
|
135
|
+
|
136
|
+
# Helper to render a browsable API for `html` format, along with basic `json`/`xml` formats, and
|
137
|
+
# with support or passing custom `kwargs` to the underlying `render` calls.
|
138
|
+
def api_response(payload, html_kwargs: nil, **kwargs)
|
139
|
+
html_kwargs ||= {}
|
140
|
+
json_kwargs = kwargs.delete(:json_kwargs) || {}
|
141
|
+
xml_kwargs = kwargs.delete(:xml_kwargs) || {}
|
142
|
+
|
143
|
+
# Raise helpful error if payload is nil. Usually this happens when a record is not found (e.g.,
|
144
|
+
# when passing something like `User.find_by(id: some_id)` to `api_response`). The caller should
|
145
|
+
# actually be calling `find_by!` to raise ActiveRecord::RecordNotFound and allowing the REST
|
146
|
+
# framework to catch this error and return an appropriate error response.
|
147
|
+
if payload.nil?
|
148
|
+
raise RESTFramework::NilPassedToAPIResponseError
|
149
|
+
end
|
150
|
+
|
151
|
+
respond_to do |format|
|
152
|
+
if payload == ''
|
153
|
+
format.json {head :no_content} if self.serialize_to_json
|
154
|
+
format.xml {head :no_content} if self.serialize_to_xml
|
155
|
+
else
|
156
|
+
format.json {
|
157
|
+
jkwargs = kwargs.merge(json_kwargs)
|
158
|
+
render(json: payload, layout: false, **jkwargs)
|
159
|
+
} if self.serialize_to_json
|
160
|
+
format.xml {
|
161
|
+
xkwargs = kwargs.merge(xml_kwargs)
|
162
|
+
render(xml: payload, layout: false, **xkwargs)
|
163
|
+
} if self.serialize_to_xml
|
164
|
+
# TODO: possibly support more formats here if supported?
|
165
|
+
end
|
166
|
+
format.html {
|
167
|
+
@payload = payload
|
168
|
+
if payload == ''
|
169
|
+
@json_payload = '' if self.serialize_to_json
|
170
|
+
@xml_payload = '' if self.serialize_to_xml
|
96
171
|
else
|
97
|
-
|
98
|
-
|
99
|
-
kwargs = kwargs.merge(json_kwargs)
|
100
|
-
render(json: payload, layout: false, **kwargs)
|
101
|
-
}
|
102
|
-
end
|
103
|
-
if payload.respond_to?(:to_xml)
|
104
|
-
format.xml {
|
105
|
-
kwargs = kwargs.merge(xml_kwargs)
|
106
|
-
render(xml: payload, layout: false, **kwargs)
|
107
|
-
}
|
108
|
-
end
|
172
|
+
@json_payload = payload.to_json if self.serialize_to_json
|
173
|
+
@xml_payload = payload.to_xml if self.serialize_to_xml
|
109
174
|
end
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
begin
|
123
|
-
render(**kwargs)
|
124
|
-
rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
|
125
|
-
kwargs[:layout] = "rest_framework"
|
126
|
-
kwargs[:template] = "rest_framework/default"
|
127
|
-
end
|
128
|
-
render(**kwargs)
|
129
|
-
}
|
130
|
-
end
|
175
|
+
@template_logo_text ||= "Rails REST Framework"
|
176
|
+
@title ||= self.controller_name.camelize
|
177
|
+
@routes ||= self._get_routes
|
178
|
+
hkwargs = kwargs.merge(html_kwargs)
|
179
|
+
begin
|
180
|
+
render(**hkwargs)
|
181
|
+
rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
|
182
|
+
hkwargs[:layout] = "rest_framework"
|
183
|
+
hkwargs[:template] = "rest_framework/default"
|
184
|
+
render(**hkwargs)
|
185
|
+
end
|
186
|
+
}
|
131
187
|
end
|
132
188
|
end
|
133
|
-
|
134
189
|
end
|
@@ -1,229 +1,267 @@
|
|
1
1
|
require_relative 'base'
|
2
|
-
require_relative '../
|
3
|
-
|
4
|
-
module RESTFramework
|
5
|
-
|
6
|
-
module BaseModelControllerMixin
|
7
|
-
include BaseControllerMixin
|
8
|
-
def self.included(base)
|
9
|
-
if base.is_a? Class
|
10
|
-
BaseControllerMixin.included(base)
|
11
|
-
base.class_attribute(*[
|
12
|
-
:model,
|
13
|
-
:recordset,
|
14
|
-
:fields,
|
15
|
-
:action_fields,
|
16
|
-
:native_serializer_config,
|
17
|
-
:native_serializer_action_config,
|
18
|
-
:filterset_fields,
|
19
|
-
:allowed_parameters,
|
20
|
-
:allowed_action_parameters,
|
21
|
-
:serializer_class,
|
22
|
-
:extra_member_actions,
|
23
|
-
:disable_creation_from_recordset,
|
24
|
-
])
|
25
|
-
base.alias_method(:extra_collection_actions=, :extra_actions=)
|
26
|
-
end
|
27
|
-
end
|
2
|
+
require_relative '../filters'
|
28
3
|
|
29
|
-
protected
|
30
4
|
|
31
|
-
|
32
|
-
|
33
|
-
|
5
|
+
# This module provides the core functionality for controllers based on models.
|
6
|
+
module RESTFramework::BaseModelControllerMixin
|
7
|
+
include RESTFramework::BaseControllerMixin
|
34
8
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
action = self.action_name.to_sym
|
9
|
+
def self.included(base)
|
10
|
+
if base.is_a? Class
|
11
|
+
RESTFramework::BaseControllerMixin.included(base)
|
39
12
|
|
40
|
-
#
|
41
|
-
|
13
|
+
# Add class attributes (with defaults) unless they already exist.
|
14
|
+
{
|
15
|
+
# Core attributes related to models.
|
16
|
+
model: nil,
|
17
|
+
recordset: nil,
|
42
18
|
|
43
|
-
|
44
|
-
|
19
|
+
# Attributes for configuring record fields.
|
20
|
+
fields: nil,
|
21
|
+
action_fields: nil,
|
45
22
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
action = self.action_name.to_sym
|
23
|
+
# Attributes for create/update parameters.
|
24
|
+
allowed_parameters: nil,
|
25
|
+
allowed_action_parameters: nil,
|
50
26
|
|
51
|
-
|
52
|
-
|
27
|
+
# Attributes for the default native serializer.
|
28
|
+
native_serializer_config: nil,
|
29
|
+
native_serializer_singular_config: nil,
|
30
|
+
native_serializer_plural_config: nil,
|
53
31
|
|
54
|
-
|
32
|
+
# Attributes for default model filtering (and ordering).
|
33
|
+
filterset_fields: nil,
|
34
|
+
ordering_fields: nil,
|
35
|
+
ordering_query_param: 'ordering',
|
36
|
+
|
37
|
+
# Other misc attributes.
|
38
|
+
disable_creation_from_recordset: false, # Option to disable `recordset.create` behavior.
|
39
|
+
}.each do |a, default|
|
40
|
+
unless base.respond_to?(a)
|
41
|
+
base.class_attribute(a)
|
42
|
+
|
43
|
+
# Set default manually so we can still support Rails 4. Maybe later we can use the default
|
44
|
+
# parameter on `class_attribute`.
|
45
|
+
base.send(:"#{a}=", default)
|
46
|
+
end
|
47
|
+
end
|
55
48
|
end
|
49
|
+
end
|
56
50
|
|
57
|
-
|
58
|
-
def get_allowed_parameters
|
59
|
-
allowed_action_parameters = self.class.allowed_action_parameters || {}
|
60
|
-
action = self.action_name.to_sym
|
51
|
+
protected
|
61
52
|
|
62
|
-
|
63
|
-
|
53
|
+
def _get_specific_action_config(action_config_key, generic_config_key)
|
54
|
+
action_config = self.class.send(action_config_key) || {}
|
55
|
+
action = self.action_name&.to_sym
|
64
56
|
|
65
|
-
|
66
|
-
|
57
|
+
# Index action should use :list serializer if :index is not provided.
|
58
|
+
action = :list if action == :index && !action_config.key?(:index)
|
59
|
+
|
60
|
+
return (action_config[action] if action) || self.class.send(generic_config_key)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get a list of parameters allowed for the current action.
|
64
|
+
def get_allowed_parameters
|
65
|
+
return _get_specific_action_config(:allowed_action_parameters, :allowed_parameters)&.map(&:to_s)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get a list of fields for the current action.
|
69
|
+
def get_fields
|
70
|
+
return (
|
71
|
+
_get_specific_action_config(:action_fields, :fields)&.map(&:to_s) ||
|
72
|
+
self.get_model&.column_names ||
|
73
|
+
[]
|
74
|
+
)
|
75
|
+
end
|
67
76
|
|
68
|
-
|
69
|
-
|
77
|
+
# Helper to get the configured serializer class, or `NativeSerializer` as a default.
|
78
|
+
# @return [RESTFramework::BaseSerializer]
|
79
|
+
def get_serializer_class
|
80
|
+
return self.class.serializer_class || RESTFramework::NativeSerializer
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get the list of filtering backends to use.
|
84
|
+
# @return [RESTFramework::BaseFilter]
|
85
|
+
def get_filter_backends
|
86
|
+
return self.class.filter_backends || [
|
87
|
+
RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter
|
88
|
+
]
|
89
|
+
end
|
90
|
+
|
91
|
+
# Filter the request body for keys in current action's allowed_parameters/fields config.
|
92
|
+
def get_body_params
|
93
|
+
return @_get_body_params ||= begin
|
70
94
|
fields = self.get_allowed_parameters || self.get_fields
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
alias :get_create_params :_get_parameter_values_from_request_body
|
76
|
-
alias :get_update_params :_get_parameter_values_from_request_body
|
77
|
-
|
78
|
-
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
79
|
-
def _get_filterset_values_from_params
|
80
|
-
fields = self.filterset_fields || self.get_fields
|
81
|
-
return @_get_filterset_values_from_params ||= request.query_parameters.select { |p|
|
82
|
-
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
95
|
+
|
96
|
+
# Filter the request body.
|
97
|
+
body_params = request.request_parameters.select { |p|
|
98
|
+
fields.include?(p)
|
83
99
|
}
|
84
|
-
end
|
85
|
-
alias :get_lookup_params :_get_filterset_values_from_params
|
86
|
-
alias :get_filter_params :_get_filterset_values_from_params
|
87
|
-
|
88
|
-
# Get the recordset, filtered by the filter params.
|
89
|
-
def get_filtered_recordset
|
90
|
-
filter_params = self.get_filter_params
|
91
|
-
unless filter_params.blank?
|
92
|
-
return self.get_recordset.where(**self.get_filter_params.to_hash.symbolize_keys)
|
93
|
-
end
|
94
|
-
return self.get_recordset
|
95
|
-
end
|
96
100
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
return records[0]
|
101
|
+
# Add query params in place of missing body params, if configured.
|
102
|
+
if self.class.accept_generic_params_as_body_params
|
103
|
+
(fields - body_params.keys).each do |k|
|
104
|
+
if (value = params[k])
|
105
|
+
body_params[k] = value
|
106
|
+
end
|
107
|
+
end
|
105
108
|
end
|
106
|
-
return nil
|
107
|
-
end
|
108
109
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
return self.class.model if self.class.model
|
113
|
-
unless from_internal_get_recordset # prevent infinite recursion
|
114
|
-
recordset = self._get_recordset(from_internal_get_model: true)
|
115
|
-
return (@model = recordset.klass) if recordset
|
110
|
+
# Filter primary key if configured.
|
111
|
+
if self.class.filter_pk_from_request_body
|
112
|
+
body_params.delete(self.get_model&.primary_key)
|
116
113
|
end
|
117
|
-
|
118
|
-
|
119
|
-
|
114
|
+
|
115
|
+
# Filter fields in exclude_body_fields.
|
116
|
+
(self.class.exclude_body_fields || []).each do |f|
|
117
|
+
body_params.delete(f.to_s)
|
120
118
|
end
|
121
|
-
|
119
|
+
|
120
|
+
body_params
|
122
121
|
end
|
122
|
+
end
|
123
|
+
alias :get_create_params :get_body_params
|
124
|
+
alias :get_update_params :get_body_params
|
123
125
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
return self.
|
128
|
-
unless from_internal_get_model # prevent infinite recursion
|
129
|
-
model = self._get_model(from_internal_get_recordset: true)
|
130
|
-
return (@recordset = model.all) if model
|
131
|
-
end
|
132
|
-
return nil
|
126
|
+
# Get a record by the primary key from the (non-filtered) recordset.
|
127
|
+
def get_record
|
128
|
+
if pk = params[self.get_model.primary_key]
|
129
|
+
return self.get_recordset.find(pk)
|
133
130
|
end
|
131
|
+
return nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# Get the model for this controller.
|
135
|
+
def get_model(from_get_recordset: false)
|
136
|
+
return @model if instance_variable_defined?(:@model) && @model
|
137
|
+
return (@model = self.class.model) if self.class.model
|
134
138
|
|
135
|
-
#
|
136
|
-
|
137
|
-
|
139
|
+
# Delegate to the recordset's model, if it's defined.
|
140
|
+
unless from_get_recordset # prevent infinite recursion
|
141
|
+
if (recordset = self.get_recordset)
|
142
|
+
return @model = recordset.klass
|
143
|
+
end
|
138
144
|
end
|
139
145
|
|
140
|
-
#
|
141
|
-
|
142
|
-
return
|
146
|
+
# Try to determine model from controller name.
|
147
|
+
begin
|
148
|
+
return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
|
149
|
+
rescue NameError
|
143
150
|
end
|
151
|
+
|
152
|
+
return nil
|
144
153
|
end
|
145
154
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
return
|
155
|
+
# Get the set of records this controller has access to.
|
156
|
+
def get_recordset
|
157
|
+
return @recordset if instance_variable_defined?(:@recordset) && @recordset
|
158
|
+
return (@recordset = self.class.recordset) if self.class.recordset
|
159
|
+
|
160
|
+
# If there is a model, return that model's default scope (all records by default).
|
161
|
+
if (model = self.get_model(from_get_recordset: true))
|
162
|
+
return @recordset = model.all
|
154
163
|
end
|
164
|
+
|
165
|
+
return nil
|
155
166
|
end
|
167
|
+
end
|
168
|
+
|
156
169
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
170
|
+
# Mixin for listing records.
|
171
|
+
module RESTFramework::ListModelMixin
|
172
|
+
def index
|
173
|
+
@records = self.get_filtered_data(self.get_recordset)
|
174
|
+
|
175
|
+
# Handle pagination, if enabled.
|
176
|
+
if self.class.paginator_class
|
177
|
+
paginator = self.class.paginator_class.new(data: @records, controller: self)
|
178
|
+
page = paginator.get_page
|
179
|
+
serialized_page = self.get_serializer_class.new(object: page, controller: self).serialize
|
180
|
+
data = paginator.get_paginated_response(serialized_page)
|
181
|
+
else
|
182
|
+
data = self.get_serializer_class.new(object: @records, controller: self).serialize
|
164
183
|
end
|
184
|
+
|
185
|
+
return api_response(data)
|
165
186
|
end
|
187
|
+
end
|
166
188
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
@record = self.get_model.create!(self.get_create_params)
|
175
|
-
end
|
176
|
-
@serialized_record = self.get_serializer_class.new(
|
177
|
-
object: @record, controller: self
|
178
|
-
).serialize
|
179
|
-
return api_response(@serialized_record)
|
180
|
-
end
|
189
|
+
|
190
|
+
# Mixin for showing records.
|
191
|
+
module RESTFramework::ShowModelMixin
|
192
|
+
def show
|
193
|
+
@record = self.get_record
|
194
|
+
serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
|
195
|
+
return api_response(serialized_record)
|
181
196
|
end
|
197
|
+
end
|
198
|
+
|
182
199
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
200
|
+
# Mixin for creating records.
|
201
|
+
module RESTFramework::CreateModelMixin
|
202
|
+
def create
|
203
|
+
if self.get_recordset.respond_to?(:create!) && !self.disable_creation_from_recordset
|
204
|
+
# Create with any properties inherited from the recordset (like associations).
|
205
|
+
@record = self.get_recordset.create!(self.get_create_params)
|
206
|
+
else
|
207
|
+
# Otherwise, perform a "bare" create.
|
208
|
+
@record = self.get_model.create!(self.get_create_params)
|
191
209
|
end
|
210
|
+
serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
|
211
|
+
return api_response(serialized_record)
|
192
212
|
end
|
213
|
+
end
|
193
214
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
215
|
+
|
216
|
+
# Mixin for updating records.
|
217
|
+
module RESTFramework::UpdateModelMixin
|
218
|
+
def update
|
219
|
+
@record = self.get_record
|
220
|
+
@record.update!(self.get_update_params)
|
221
|
+
serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
|
222
|
+
return api_response(serialized_record)
|
200
223
|
end
|
224
|
+
end
|
201
225
|
|
202
|
-
module ReadOnlyModelControllerMixin
|
203
|
-
include BaseModelControllerMixin
|
204
|
-
def self.included(base)
|
205
|
-
if base.is_a? Class
|
206
|
-
BaseModelControllerMixin.included(base)
|
207
|
-
end
|
208
|
-
end
|
209
226
|
|
210
|
-
|
211
|
-
|
227
|
+
# Mixin for destroying records.
|
228
|
+
module RESTFramework::DestroyModelMixin
|
229
|
+
def destroy
|
230
|
+
@record = self.get_record
|
231
|
+
@record.destroy!
|
232
|
+
api_response('')
|
212
233
|
end
|
234
|
+
end
|
213
235
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
236
|
+
|
237
|
+
# Mixin that includes show/list mixins.
|
238
|
+
module RESTFramework::ReadOnlyModelControllerMixin
|
239
|
+
include RESTFramework::BaseModelControllerMixin
|
240
|
+
|
241
|
+
def self.included(base)
|
242
|
+
if base.is_a? Class
|
243
|
+
RESTFramework::BaseModelControllerMixin.included(base)
|
220
244
|
end
|
245
|
+
end
|
246
|
+
|
247
|
+
include RESTFramework::ListModelMixin
|
248
|
+
include RESTFramework::ShowModelMixin
|
249
|
+
end
|
221
250
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
251
|
+
|
252
|
+
# Mixin that includes all the CRUD mixins.
|
253
|
+
module RESTFramework::ModelControllerMixin
|
254
|
+
include RESTFramework::BaseModelControllerMixin
|
255
|
+
|
256
|
+
def self.included(base)
|
257
|
+
if base.is_a? Class
|
258
|
+
RESTFramework::BaseModelControllerMixin.included(base)
|
259
|
+
end
|
227
260
|
end
|
228
261
|
|
262
|
+
include RESTFramework::ListModelMixin
|
263
|
+
include RESTFramework::ShowModelMixin
|
264
|
+
include RESTFramework::CreateModelMixin
|
265
|
+
include RESTFramework::UpdateModelMixin
|
266
|
+
include RESTFramework::DestroyModelMixin
|
229
267
|
end
|