rest_framework 0.1.0 → 0.2.2
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 +14 -8
- 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 -159
- data/lib/rest_framework/controller_mixins/models.rb +215 -176
- 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 +10 -4
- data/lib/rest_framework/serializers.rb +133 -84
- data/lib/rest_framework/version.rb +11 -9
- 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: 37a40fca2be23a6fe5fb1f982349ce5dd212644ffe450d979d04c339360b22eb
|
4
|
+
data.tar.gz: 3e036f7150018a46756efc378dacf16321a9394cb9d04d9b4af7220158400cda
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c4a665ccde25d1c53423c168d1e5849d4ba4b91a2a0bcf6fb1f3b3714b624112d951c06aa84029aeaeb7c3703b30e8c73824688070370b3f0e9e3c3d2fd70ec
|
7
|
+
data.tar.gz: 6a4511553767ed27726a9d12daf0cd46958fe2098566f0daff5c1f9056bfbfa8c26836646b76e3f4a22a1efbea341dc6cb682a04c7c1b5b7635b46cc56548c3a
|
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,7 +116,7 @@ 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
|
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.2
|
@@ -1,194 +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
|
-
paginator_class: nil,
|
40
|
-
page_size: nil,
|
41
|
-
page_query_param: 'page',
|
42
|
-
page_size_query_param: 'page_size',
|
43
|
-
max_page_size: nil,
|
44
|
-
serializer_class: nil,
|
45
|
-
singleton_controller: nil,
|
46
|
-
skip_actions: nil,
|
47
|
-
}.each do |a, default|
|
48
|
-
unless base.respond_to?(a)
|
49
|
-
base.class_attribute(a)
|
50
|
-
|
51
|
-
# Set default manually so we can still support Rails 4. Maybe later we can use the
|
52
|
-
# default parameter on `class_attribute`.
|
53
|
-
base.send(:"#{a}=", default)
|
54
|
-
end
|
55
|
-
end
|
27
|
+
return skip
|
28
|
+
end
|
29
|
+
end
|
56
30
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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)
|
61
60
|
end
|
61
|
+
end
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
|
68
|
-
base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
|
69
|
-
base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
|
70
|
-
base.rescue_from(ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed)
|
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=)
|
71
67
|
end
|
72
|
-
end
|
73
68
|
|
74
|
-
|
69
|
+
# Skip csrf since this is an API.
|
70
|
+
base.skip_before_action(:verify_authenticity_token) rescue nil
|
75
71
|
|
76
|
-
|
77
|
-
|
78
|
-
|
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)
|
79
77
|
end
|
78
|
+
end
|
80
79
|
|
81
|
-
|
82
|
-
def get_filtered_data(data)
|
83
|
-
self.get_filter_backends.each do |filter_class|
|
84
|
-
filter = filter_class.new(controller: self)
|
85
|
-
data = filter.get_filtered_data(data)
|
86
|
-
end
|
80
|
+
protected
|
87
81
|
|
88
|
-
|
89
|
-
|
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
|
90
87
|
|
91
|
-
|
92
|
-
|
93
|
-
|
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)
|
94
93
|
end
|
95
94
|
|
96
|
-
|
97
|
-
|
98
|
-
action_serializer_config = self.class.native_serializer_action_config || {}
|
99
|
-
action = self.action_name.to_sym
|
95
|
+
return data
|
96
|
+
end
|
100
97
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
106
103
|
|
107
|
-
|
108
|
-
|
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
109
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
)
|
114
|
-
end
|
110
|
+
def record_not_found(e)
|
111
|
+
return api_response({message: "Record not found.", exception: e}, status: 404)
|
112
|
+
end
|
115
113
|
|
116
|
-
|
117
|
-
|
118
|
-
|
114
|
+
def record_not_saved(e)
|
115
|
+
return api_response({message: "Record not saved.", exception: e}, status: 406)
|
116
|
+
end
|
119
117
|
|
120
|
-
|
121
|
-
|
122
|
-
|
118
|
+
def record_not_destroyed(e)
|
119
|
+
return api_response({message: "Record not destroyed.", exception: e}, status: 406)
|
120
|
+
end
|
123
121
|
|
124
|
-
|
125
|
-
|
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
|
126
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
|
127
135
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
140
149
|
end
|
141
150
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
156
171
|
else
|
157
|
-
|
158
|
-
|
159
|
-
kwargs = kwargs.merge(json_kwargs)
|
160
|
-
render(json: payload, layout: false, **kwargs)
|
161
|
-
}
|
162
|
-
end
|
163
|
-
if payload.respond_to?(:to_xml)
|
164
|
-
format.xml {
|
165
|
-
kwargs = kwargs.merge(xml_kwargs)
|
166
|
-
render(xml: payload, layout: false, **kwargs)
|
167
|
-
}
|
168
|
-
end
|
172
|
+
@json_payload = payload.to_json if self.serialize_to_json
|
173
|
+
@xml_payload = payload.to_xml if self.serialize_to_xml
|
169
174
|
end
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
begin
|
183
|
-
render(**kwargs)
|
184
|
-
rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
|
185
|
-
kwargs[:layout] = "rest_framework"
|
186
|
-
kwargs[:template] = "rest_framework/default"
|
187
|
-
render(**kwargs)
|
188
|
-
end
|
189
|
-
}
|
190
|
-
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
|
+
}
|
191
187
|
end
|
192
188
|
end
|
193
|
-
|
194
189
|
end
|
@@ -1,228 +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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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)
|
45
46
|
end
|
46
47
|
end
|
47
48
|
end
|
49
|
+
end
|
48
50
|
|
49
|
-
|
51
|
+
protected
|
50
52
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
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
|
55
56
|
|
56
|
-
#
|
57
|
-
|
58
|
-
allowed_action_parameters = self.class.allowed_action_parameters || {}
|
59
|
-
action = self.action_name.to_sym
|
57
|
+
# Index action should use :list serializer if :index is not provided.
|
58
|
+
action = :list if action == :index && !action_config.key?(:index)
|
60
59
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
|
65
|
-
end
|
60
|
+
return (action_config[action] if action) || self.class.send(generic_config_key)
|
61
|
+
end
|
66
62
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
]
|
72
|
-
end
|
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
|
73
67
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
78
76
|
|
79
|
-
|
80
|
-
|
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
|
81
82
|
|
82
|
-
|
83
|
-
|
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
|
84
90
|
|
85
|
-
|
86
|
-
|
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
|
87
94
|
fields = self.get_allowed_parameters || self.get_fields
|
88
|
-
return @get_body_params ||= (request.request_parameters.select { |p|
|
89
|
-
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
90
|
-
})
|
91
|
-
end
|
92
|
-
alias :get_create_params :get_body_params
|
93
|
-
alias :get_update_params :get_body_params
|
94
|
-
|
95
|
-
# Get a record by `id` or return a single record if recordset is filtered down to a single
|
96
|
-
# record.
|
97
|
-
def get_record
|
98
|
-
records = self.get_filtered_data(self.get_recordset)
|
99
|
-
if params['id'] # direct lookup
|
100
|
-
return records.find(params['id'])
|
101
|
-
elsif records.length == 1
|
102
|
-
return records[0]
|
103
|
-
end
|
104
|
-
return nil
|
105
|
-
end
|
106
95
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
114
108
|
end
|
115
|
-
|
116
|
-
|
117
|
-
|
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)
|
118
113
|
end
|
119
|
-
return nil
|
120
|
-
end
|
121
114
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
return self.class.recordset if self.class.recordset
|
126
|
-
unless from_internal_get_model # prevent infinite recursion
|
127
|
-
model = self._get_model(from_internal_get_recordset: true)
|
128
|
-
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)
|
129
118
|
end
|
130
|
-
return nil
|
131
|
-
end
|
132
119
|
|
133
|
-
|
134
|
-
def get_model
|
135
|
-
return _get_model
|
120
|
+
body_params
|
136
121
|
end
|
122
|
+
end
|
123
|
+
alias :get_create_params :get_body_params
|
124
|
+
alias :get_update_params :get_body_params
|
137
125
|
|
138
|
-
|
139
|
-
|
140
|
-
|
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)
|
141
130
|
end
|
131
|
+
return nil
|
142
132
|
end
|
143
133
|
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
147
138
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
serialized_page = self.get_serializer_class.new(object: page, controller: self).serialize
|
153
|
-
data = paginator.get_paginated_response(serialized_page)
|
154
|
-
else
|
155
|
-
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
|
156
143
|
end
|
144
|
+
end
|
157
145
|
|
158
|
-
|
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
|
159
150
|
end
|
151
|
+
|
152
|
+
return nil
|
160
153
|
end
|
161
154
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
167
163
|
end
|
164
|
+
|
165
|
+
return nil
|
168
166
|
end
|
167
|
+
end
|
169
168
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
181
183
|
end
|
184
|
+
|
185
|
+
return api_response(data)
|
182
186
|
end
|
187
|
+
end
|
183
188
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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)
|
191
196
|
end
|
197
|
+
end
|
192
198
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
199
|
+
|
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)
|
198
209
|
end
|
210
|
+
serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
|
211
|
+
return api_response(serialized_record)
|
199
212
|
end
|
213
|
+
end
|
200
214
|
|
201
|
-
module ReadOnlyModelControllerMixin
|
202
|
-
include BaseModelControllerMixin
|
203
|
-
def self.included(base)
|
204
|
-
if base.is_a? Class
|
205
|
-
BaseModelControllerMixin.included(base)
|
206
|
-
end
|
207
|
-
end
|
208
215
|
|
209
|
-
|
210
|
-
|
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)
|
211
223
|
end
|
224
|
+
end
|
212
225
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
226
|
+
|
227
|
+
# Mixin for destroying records.
|
228
|
+
module RESTFramework::DestroyModelMixin
|
229
|
+
def destroy
|
230
|
+
@record = self.get_record
|
231
|
+
@record.destroy!
|
232
|
+
api_response('')
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
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)
|
219
244
|
end
|
245
|
+
end
|
246
|
+
|
247
|
+
include RESTFramework::ListModelMixin
|
248
|
+
include RESTFramework::ShowModelMixin
|
249
|
+
end
|
220
250
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
226
260
|
end
|
227
261
|
|
262
|
+
include RESTFramework::ListModelMixin
|
263
|
+
include RESTFramework::ShowModelMixin
|
264
|
+
include RESTFramework::CreateModelMixin
|
265
|
+
include RESTFramework::UpdateModelMixin
|
266
|
+
include RESTFramework::DestroyModelMixin
|
228
267
|
end
|