rest_framework 0.0.16 → 0.2.1
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 +16 -9
- data/lib/rest_framework.rb +3 -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 +154 -166
- data/lib/rest_framework/controller_mixins/models.rb +215 -169
- data/lib/rest_framework/errors.rb +26 -0
- data/lib/rest_framework/filters.rb +53 -50
- data/lib/rest_framework/generators.rb +5 -0
- data/lib/rest_framework/generators/controller_generator.rb +26 -0
- data/lib/rest_framework/paginators.rb +76 -65
- data/lib/rest_framework/routers.rb +93 -44
- data/lib/rest_framework/serializers.rb +131 -93
- data/lib/rest_framework/version.rb +11 -9
- metadata +8 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3af970dedaa74cde70348a09e74b190878e4a429a4db0318b093c581f810a349
|
4
|
+
data.tar.gz: 5ccd0cf76d51d9d5d3035eee80e73e1234b13c974d04bbd4e80a48bc5b82d714
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a5841e302bdf9f2ae56748c2c81e94338a9c77c0dde248218d48733ff9bda28dc4a42e9cb5b21c339abf596490bb6de21e18a1f43bb8ef80802ce8d8db4d925e
|
7
|
+
data.tar.gz: 0b395e37ac08c45e3bf8d3940dde55fba063cb43b3e8125e4c9a55dbf6dc15e985c1fa72e51a042e4563194b0f4fa5f33dd3fa5e96de456eaf9448a27b7f76ad
|
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,11 +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
125
|
To interact with the test app, `cd test` and operate it via the normal Rails interfaces. Ensure you
|
120
|
-
run `rake db:schema:load` before running `rails server` or `rails console`.
|
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`.
|
data/lib/rest_framework.rb
CHANGED
@@ -1,10 +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"
|
6
8
|
require_relative "rest_framework/filters"
|
7
9
|
require_relative "rest_framework/paginators"
|
8
10
|
require_relative "rest_framework/routers"
|
9
11
|
require_relative "rest_framework/serializers"
|
10
12
|
require_relative "rest_framework/version"
|
13
|
+
require_relative "rest_framework/generators"
|
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.1
|
@@ -1,201 +1,189 @@
|
|
1
|
+
require_relative '../errors'
|
1
2
|
require_relative '../serializers'
|
2
3
|
|
3
|
-
module RESTFramework
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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 || []
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
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
24
|
end
|
25
|
-
|
26
|
-
return skip
|
27
25
|
end
|
28
|
-
end
|
29
26
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
# Add class attributes (with defaults) unless they already exist.
|
35
|
-
{
|
36
|
-
extra_actions: nil,
|
37
|
-
extra_member_actions: nil,
|
38
|
-
filter_backends: nil,
|
39
|
-
native_serializer_config: nil,
|
40
|
-
native_serializer_action_config: nil,
|
41
|
-
paginator_class: nil,
|
42
|
-
page_size: nil,
|
43
|
-
page_query_param: 'page',
|
44
|
-
page_size_query_param: 'page_size',
|
45
|
-
max_page_size: nil,
|
46
|
-
serializer_class: nil,
|
47
|
-
singleton_controller: nil,
|
48
|
-
skip_actions: nil,
|
49
|
-
}.each do |a, default|
|
50
|
-
unless base.respond_to?(a)
|
51
|
-
base.class_attribute(a)
|
52
|
-
|
53
|
-
# Set default manually so we can still support Rails 4. Maybe later we can use the
|
54
|
-
# default parameter on `class_attribute`.
|
55
|
-
base.send(:"#{a}=", default)
|
56
|
-
end
|
57
|
-
end
|
27
|
+
return skip
|
28
|
+
end
|
29
|
+
end
|
58
30
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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)
|
63
60
|
end
|
64
|
-
|
65
|
-
# skip csrf since this is an API
|
66
|
-
base.skip_before_action(:verify_authenticity_token) rescue nil
|
67
|
-
|
68
|
-
# handle some common exceptions
|
69
|
-
base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
|
70
|
-
base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
|
71
|
-
base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
|
72
|
-
base.rescue_from(ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed)
|
73
61
|
end
|
74
|
-
end
|
75
|
-
|
76
|
-
protected
|
77
62
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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=)
|
82
67
|
end
|
83
68
|
|
84
|
-
#
|
85
|
-
|
69
|
+
# Skip csrf since this is an API.
|
70
|
+
base.skip_before_action(:verify_authenticity_token) rescue nil
|
71
|
+
|
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)
|
86
77
|
end
|
78
|
+
end
|
87
79
|
|
88
|
-
|
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)
|
93
|
-
end
|
80
|
+
protected
|
94
81
|
|
95
|
-
|
96
|
-
|
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
|
97
87
|
|
98
|
-
|
99
|
-
|
100
|
-
|
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)
|
101
93
|
end
|
102
94
|
|
103
|
-
|
104
|
-
|
105
|
-
action_serializer_config = self.class.native_serializer_action_config || {}
|
106
|
-
action = self.action_name.to_sym
|
95
|
+
return data
|
96
|
+
end
|
107
97
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
113
103
|
|
114
|
-
|
115
|
-
|
104
|
+
def record_invalid(e)
|
105
|
+
return api_response(
|
106
|
+
{message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
|
107
|
+
)
|
108
|
+
end
|
116
109
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
)
|
121
|
-
end
|
110
|
+
def record_not_found(e)
|
111
|
+
return api_response({message: "Record not found.", exception: e}, status: 404)
|
112
|
+
end
|
122
113
|
|
123
|
-
|
124
|
-
|
125
|
-
|
114
|
+
def record_not_saved(e)
|
115
|
+
return api_response({message: "Record not saved.", exception: e}, status: 406)
|
116
|
+
end
|
126
117
|
|
127
|
-
|
128
|
-
|
129
|
-
|
118
|
+
def record_not_destroyed(e)
|
119
|
+
return api_response({message: "Record not destroyed.", exception: e}, status: 406)
|
120
|
+
end
|
130
121
|
|
131
|
-
|
132
|
-
|
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
|
133
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
|
134
135
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
147
149
|
end
|
148
150
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
163
171
|
else
|
164
|
-
|
165
|
-
|
166
|
-
kwargs = kwargs.merge(json_kwargs)
|
167
|
-
render(json: payload, layout: false, **kwargs)
|
168
|
-
}
|
169
|
-
end
|
170
|
-
if payload.respond_to?(:to_xml)
|
171
|
-
format.xml {
|
172
|
-
kwargs = kwargs.merge(xml_kwargs)
|
173
|
-
render(xml: payload, layout: false, **kwargs)
|
174
|
-
}
|
175
|
-
end
|
172
|
+
@json_payload = payload.to_json if self.serialize_to_json
|
173
|
+
@xml_payload = payload.to_xml if self.serialize_to_xml
|
176
174
|
end
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
begin
|
190
|
-
render(**kwargs)
|
191
|
-
rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
|
192
|
-
kwargs[:layout] = "rest_framework"
|
193
|
-
kwargs[:template] = "rest_framework/default"
|
194
|
-
render(**kwargs)
|
195
|
-
end
|
196
|
-
}
|
197
|
-
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
|
+
}
|
198
187
|
end
|
199
188
|
end
|
200
|
-
|
201
189
|
end
|
@@ -1,221 +1,267 @@
|
|
1
1
|
require_relative 'base'
|
2
2
|
require_relative '../filters'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
4
|
+
|
5
|
+
# This module provides the core functionality for controllers based on models.
|
6
|
+
module RESTFramework::BaseModelControllerMixin
|
7
|
+
include RESTFramework::BaseControllerMixin
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
if base.is_a? Class
|
11
|
+
RESTFramework::BaseControllerMixin.included(base)
|
12
|
+
|
13
|
+
# Add class attributes (with defaults) unless they already exist.
|
14
|
+
{
|
15
|
+
# Core attributes related to models.
|
16
|
+
model: nil,
|
17
|
+
recordset: nil,
|
18
|
+
|
19
|
+
# Attributes for configuring record fields.
|
20
|
+
fields: nil,
|
21
|
+
action_fields: nil,
|
22
|
+
|
23
|
+
# Attributes for create/update parameters.
|
24
|
+
allowed_parameters: nil,
|
25
|
+
allowed_action_parameters: nil,
|
26
|
+
|
27
|
+
# Attributes for the default native serializer.
|
28
|
+
native_serializer_config: nil,
|
29
|
+
native_serializer_singular_config: nil,
|
30
|
+
native_serializer_plural_config: nil,
|
31
|
+
|
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)
|
41
46
|
end
|
42
47
|
end
|
43
48
|
end
|
49
|
+
end
|
44
50
|
|
45
|
-
|
51
|
+
protected
|
46
52
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
action = self.action_name.to_sym
|
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
|
51
56
|
|
52
|
-
|
53
|
-
|
57
|
+
# Index action should use :list serializer if :index is not provided.
|
58
|
+
action = :list if action == :index && !action_config.key?(:index)
|
54
59
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
# Get the list of filtering backends to use.
|
59
|
-
def get_filter_backends
|
60
|
-
backends = super
|
61
|
-
return backends if backends
|
60
|
+
return (action_config[action] if action) || self.class.send(generic_config_key)
|
61
|
+
end
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
66
67
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
71
76
|
|
72
|
-
|
73
|
-
|
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
|
74
82
|
|
75
|
-
|
76
|
-
|
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
|
77
90
|
|
78
|
-
|
79
|
-
|
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
|
80
94
|
fields = self.get_allowed_parameters || self.get_fields
|
81
|
-
return @_get_parameter_values_from_request_body ||= (request.request_parameters.select { |p|
|
82
|
-
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
83
|
-
})
|
84
|
-
end
|
85
|
-
alias :get_create_params :_get_parameter_values_from_request_body
|
86
|
-
alias :get_update_params :_get_parameter_values_from_request_body
|
87
|
-
|
88
|
-
# Get a record by `id` or return a single record if recordset is filtered down to a single
|
89
|
-
# record.
|
90
|
-
def get_record
|
91
|
-
records = self.get_filtered_data(self.get_recordset)
|
92
|
-
if params['id'] # direct lookup
|
93
|
-
return records.find(params['id'])
|
94
|
-
elsif records.length == 1
|
95
|
-
return records[0]
|
96
|
-
end
|
97
|
-
return nil
|
98
|
-
end
|
99
95
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
96
|
+
# Filter the request body.
|
97
|
+
body_params = request.request_parameters.select { |p|
|
98
|
+
fields.include?(p)
|
99
|
+
}
|
100
|
+
|
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
|
107
108
|
end
|
108
|
-
|
109
|
-
|
110
|
-
|
109
|
+
|
110
|
+
# Filter primary key if configured.
|
111
|
+
if self.class.filter_pk_from_request_body
|
112
|
+
body_params.delete(self.get_model&.primary_key)
|
111
113
|
end
|
112
|
-
return nil
|
113
|
-
end
|
114
114
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
return self.class.recordset if self.class.recordset
|
119
|
-
unless from_internal_get_model # prevent infinite recursion
|
120
|
-
model = self._get_model(from_internal_get_recordset: true)
|
121
|
-
return (@recordset = model.all) if model
|
115
|
+
# Filter fields in exclude_body_fields.
|
116
|
+
(self.class.exclude_body_fields || []).each do |f|
|
117
|
+
body_params.delete(f.to_s)
|
122
118
|
end
|
123
|
-
return nil
|
124
|
-
end
|
125
119
|
|
126
|
-
|
127
|
-
def get_model
|
128
|
-
return _get_model
|
120
|
+
body_params
|
129
121
|
end
|
122
|
+
end
|
123
|
+
alias :get_create_params :get_body_params
|
124
|
+
alias :get_update_params :get_body_params
|
130
125
|
|
131
|
-
|
132
|
-
|
133
|
-
|
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)
|
134
130
|
end
|
131
|
+
return nil
|
135
132
|
end
|
136
133
|
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
140
138
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
serialized_page = self.get_serializer_class.new(object: page, controller: self).serialize
|
146
|
-
data = paginator.get_paginated_response(serialized_page)
|
147
|
-
else
|
148
|
-
data = self.get_serializer_class.new(object: @records, controller: self).serialize
|
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
|
149
143
|
end
|
144
|
+
end
|
150
145
|
|
151
|
-
|
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
|
152
150
|
end
|
151
|
+
|
152
|
+
return nil
|
153
153
|
end
|
154
154
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
160
163
|
end
|
164
|
+
|
165
|
+
return nil
|
161
166
|
end
|
167
|
+
end
|
162
168
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
169
|
+
|
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
|
174
183
|
end
|
184
|
+
|
185
|
+
return api_response(data)
|
175
186
|
end
|
187
|
+
end
|
176
188
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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)
|
184
196
|
end
|
197
|
+
end
|
198
|
+
|
185
199
|
|
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
|
-
module ReadOnlyModelControllerMixin
|
195
|
-
include BaseModelControllerMixin
|
196
|
-
def self.included(base)
|
197
|
-
if base.is_a? Class
|
198
|
-
BaseModelControllerMixin.included(base)
|
199
|
-
end
|
200
|
-
end
|
201
215
|
|
202
|
-
|
203
|
-
|
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)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
|
227
|
+
# Mixin for destroying records.
|
228
|
+
module RESTFramework::DestroyModelMixin
|
229
|
+
def destroy
|
230
|
+
@record = self.get_record
|
231
|
+
@record.destroy!
|
232
|
+
api_response('')
|
204
233
|
end
|
234
|
+
end
|
205
235
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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)
|
212
244
|
end
|
245
|
+
end
|
246
|
+
|
247
|
+
include RESTFramework::ListModelMixin
|
248
|
+
include RESTFramework::ShowModelMixin
|
249
|
+
end
|
213
250
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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
|
219
260
|
end
|
220
261
|
|
262
|
+
include RESTFramework::ListModelMixin
|
263
|
+
include RESTFramework::ShowModelMixin
|
264
|
+
include RESTFramework::CreateModelMixin
|
265
|
+
include RESTFramework::UpdateModelMixin
|
266
|
+
include RESTFramework::DestroyModelMixin
|
221
267
|
end
|