rest_framework 0.4.1 → 0.5.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/LICENSE +1 -1
- data/VERSION +1 -1
- data/lib/rest_framework/controller_mixins/base.rb +47 -37
- data/lib/rest_framework/controller_mixins/models.rb +49 -43
- data/lib/rest_framework/controller_mixins.rb +2 -3
- data/lib/rest_framework/errors.rb +1 -2
- data/lib/rest_framework/filters.rb +11 -10
- data/lib/rest_framework/generators/controller_generator.rb +12 -15
- data/lib/rest_framework/generators.rb +1 -2
- data/lib/rest_framework/paginators.rb +2 -4
- data/lib/rest_framework/routers.rb +12 -13
- data/lib/rest_framework/serializers.rb +102 -19
- data/lib/rest_framework/utils.rb +2 -2
- data/lib/rest_framework/version.rb +2 -2
- data/lib/rest_framework.rb +0 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e9dae13c73c724a592cf7f9f99347eae18dcc4acd135e98bcaf5f8682ea01e3
|
4
|
+
data.tar.gz: 52b5a7b3a0b3339987516e6ec2083c087b9387ceb13a2951523c339ee1509b2c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c9d79bd771ab07ffbd25edcc9970a3cc4ab39dfe730330e4fd704737773f7381bc75fd3f6169a9a6c17653cd3853da45a56a46907b05be668b63f8e921ee482
|
7
|
+
data.tar.gz: 9e5ec3f519a35f02f49d2fde38f3f793649361927e437bc243ff7eba5a38a8315c4557b001a4eb305aeec04e99fb91ea00727aa910e5f33496ea58d29ad28598
|
data/LICENSE
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.2
|
@@ -1,7 +1,6 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
require_relative
|
4
|
-
|
1
|
+
require_relative "../errors"
|
2
|
+
require_relative "../serializers"
|
3
|
+
require_relative "../utils"
|
5
4
|
|
6
5
|
# This module provides the common functionality for any controller mixins, a `root` action, and
|
7
6
|
# the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
|
@@ -30,8 +29,8 @@ module RESTFramework::BaseControllerMixin
|
|
30
29
|
end
|
31
30
|
|
32
31
|
def self.included(base)
|
33
|
-
if base.is_a?
|
34
|
-
base.extend
|
32
|
+
if base.is_a?(Class)
|
33
|
+
base.extend(ClassMethods)
|
35
34
|
|
36
35
|
# Add class attributes (with defaults) unless they already exist.
|
37
36
|
{
|
@@ -43,8 +42,8 @@ module RESTFramework::BaseControllerMixin
|
|
43
42
|
filter_backends: nil,
|
44
43
|
paginator_class: nil,
|
45
44
|
page_size: 20,
|
46
|
-
page_query_param:
|
47
|
-
page_size_query_param:
|
45
|
+
page_query_param: "page",
|
46
|
+
page_size_query_param: "page_size",
|
48
47
|
max_page_size: nil,
|
49
48
|
serializer_class: nil,
|
50
49
|
serialize_to_json: true,
|
@@ -52,13 +51,13 @@ module RESTFramework::BaseControllerMixin
|
|
52
51
|
singleton_controller: nil,
|
53
52
|
skip_actions: nil,
|
54
53
|
}.each do |a, default|
|
55
|
-
|
56
|
-
base.class_attribute(a)
|
54
|
+
next if base.respond_to?(a)
|
57
55
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
56
|
+
base.class_attribute(a)
|
57
|
+
|
58
|
+
# Set default manually so we can still support Rails 4. Maybe later we can use the default
|
59
|
+
# parameter on `class_attribute`.
|
60
|
+
base.send(:"#{a}=", default)
|
62
61
|
end
|
63
62
|
|
64
63
|
# Alias `extra_actions` to `extra_collection_actions`.
|
@@ -68,7 +67,11 @@ module RESTFramework::BaseControllerMixin
|
|
68
67
|
end
|
69
68
|
|
70
69
|
# Skip csrf since this is an API.
|
71
|
-
|
70
|
+
begin
|
71
|
+
base.skip_before_action(:verify_authenticity_token)
|
72
|
+
rescue
|
73
|
+
nil
|
74
|
+
end
|
72
75
|
|
73
76
|
# Handle some common exceptions.
|
74
77
|
base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
|
@@ -82,7 +85,14 @@ module RESTFramework::BaseControllerMixin
|
|
82
85
|
|
83
86
|
# Helper to get the configured serializer class.
|
84
87
|
def get_serializer_class
|
85
|
-
return self.class.serializer_class
|
88
|
+
return nil unless serializer_class = self.class.serializer_class
|
89
|
+
|
90
|
+
# Wrap it with an adapter if it's an active_model_serializer.
|
91
|
+
if defined?(ActiveModel::Serializer) && (serializer_class < ActiveModel::Serializer)
|
92
|
+
serializer_class = RESTFramework::ActiveModelSerializerAdapterFactory.for(serializer_class)
|
93
|
+
end
|
94
|
+
|
95
|
+
return serializer_class
|
86
96
|
end
|
87
97
|
|
88
98
|
# Helper to get filtering backends, defaulting to no backends.
|
@@ -101,9 +111,9 @@ module RESTFramework::BaseControllerMixin
|
|
101
111
|
end
|
102
112
|
|
103
113
|
def record_invalid(e)
|
104
|
-
return api_response(
|
105
|
-
message: "Record invalid.", exception: e, errors: e.record&.errors
|
106
|
-
|
114
|
+
return api_response(
|
115
|
+
{message: "Record invalid.", exception: e, errors: e.record&.errors}, status: 400
|
116
|
+
)
|
107
117
|
end
|
108
118
|
|
109
119
|
def record_not_found(e)
|
@@ -111,15 +121,15 @@ module RESTFramework::BaseControllerMixin
|
|
111
121
|
end
|
112
122
|
|
113
123
|
def record_not_saved(e)
|
114
|
-
return api_response(
|
115
|
-
message: "Record not saved.", exception: e, errors: e.record&.errors
|
116
|
-
|
124
|
+
return api_response(
|
125
|
+
{message: "Record not saved.", exception: e, errors: e.record&.errors}, status: 406
|
126
|
+
)
|
117
127
|
end
|
118
128
|
|
119
129
|
def record_not_destroyed(e)
|
120
|
-
return api_response(
|
121
|
-
message: "Record not destroyed.", exception: e, errors: e.record&.errors
|
122
|
-
|
130
|
+
return api_response(
|
131
|
+
{message: "Record not destroyed.", exception: e, errors: e.record&.errors}, status: 406
|
132
|
+
)
|
123
133
|
end
|
124
134
|
|
125
135
|
# Helper to render a browsable API for `html` format, along with basic `json`/`xml` formats, and
|
@@ -138,38 +148,38 @@ module RESTFramework::BaseControllerMixin
|
|
138
148
|
end
|
139
149
|
|
140
150
|
respond_to do |format|
|
141
|
-
if payload ==
|
142
|
-
format.json {head
|
143
|
-
format.xml {head
|
151
|
+
if payload == ""
|
152
|
+
format.json { head(:no_content) } if self.class.serialize_to_json
|
153
|
+
format.xml { head(:no_content) } if self.class.serialize_to_xml
|
144
154
|
else
|
145
155
|
format.json {
|
146
156
|
jkwargs = kwargs.merge(json_kwargs)
|
147
157
|
render(json: payload, layout: false, **jkwargs)
|
148
|
-
} if self.serialize_to_json
|
158
|
+
} if self.class.serialize_to_json
|
149
159
|
format.xml {
|
150
160
|
xkwargs = kwargs.merge(xml_kwargs)
|
151
161
|
render(xml: payload, layout: false, **xkwargs)
|
152
|
-
} if self.serialize_to_xml
|
162
|
+
} if self.class.serialize_to_xml
|
153
163
|
# TODO: possibly support more formats here if supported?
|
154
164
|
end
|
155
165
|
format.html {
|
156
166
|
@payload = payload
|
157
|
-
if payload ==
|
158
|
-
@json_payload =
|
159
|
-
@xml_payload =
|
167
|
+
if payload == ""
|
168
|
+
@json_payload = "" if self.class.serialize_to_json
|
169
|
+
@xml_payload = "" if self.class.serialize_to_xml
|
160
170
|
else
|
161
|
-
@json_payload = payload.to_json if self.serialize_to_json
|
162
|
-
@xml_payload = payload.to_xml if self.serialize_to_xml
|
171
|
+
@json_payload = payload.to_json if self.class.serialize_to_json
|
172
|
+
@xml_payload = payload.to_xml if self.class.serialize_to_xml
|
163
173
|
end
|
164
174
|
@template_logo_text ||= "Rails REST Framework"
|
165
175
|
@title ||= self.controller_name.camelize
|
166
|
-
@route_groups ||= RESTFramework::Utils
|
176
|
+
@route_groups ||= RESTFramework::Utils.get_routes(Rails.application.routes, request)
|
167
177
|
hkwargs = kwargs.merge(html_kwargs)
|
168
178
|
begin
|
169
179
|
render(**hkwargs)
|
170
180
|
rescue ActionView::MissingTemplate # fallback to rest_framework layout
|
171
181
|
hkwargs[:layout] = "rest_framework"
|
172
|
-
hkwargs[:html] =
|
182
|
+
hkwargs[:html] = ""
|
173
183
|
render(**hkwargs)
|
174
184
|
end
|
175
185
|
}
|
@@ -1,13 +1,12 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
|
1
|
+
require_relative "base"
|
2
|
+
require_relative "../filters"
|
4
3
|
|
5
4
|
# This module provides the core functionality for controllers based on models.
|
6
5
|
module RESTFramework::BaseModelControllerMixin
|
7
6
|
include RESTFramework::BaseControllerMixin
|
8
7
|
|
9
8
|
def self.included(base)
|
10
|
-
if base.is_a?
|
9
|
+
if base.is_a?(Class)
|
11
10
|
RESTFramework::BaseControllerMixin.included(base)
|
12
11
|
|
13
12
|
# Add class attributes (with defaults) unless they already exist.
|
@@ -22,7 +21,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
22
21
|
|
23
22
|
# Attributes for finding records.
|
24
23
|
find_by_fields: nil,
|
25
|
-
find_by_query_param:
|
24
|
+
find_by_query_param: "find_by",
|
26
25
|
|
27
26
|
# Attributes for create/update parameters.
|
28
27
|
allowed_parameters: nil,
|
@@ -32,27 +31,28 @@ module RESTFramework::BaseModelControllerMixin
|
|
32
31
|
native_serializer_config: nil,
|
33
32
|
native_serializer_singular_config: nil,
|
34
33
|
native_serializer_plural_config: nil,
|
34
|
+
native_serializer_except_query_param: "except",
|
35
35
|
|
36
36
|
# Attributes for default model filtering (and ordering).
|
37
37
|
filterset_fields: nil,
|
38
38
|
ordering_fields: nil,
|
39
|
-
ordering_query_param:
|
39
|
+
ordering_query_param: "ordering",
|
40
40
|
ordering_no_reorder: false,
|
41
41
|
search_fields: nil,
|
42
|
-
search_query_param:
|
42
|
+
search_query_param: "search",
|
43
43
|
search_ilike: false,
|
44
44
|
|
45
45
|
# Other misc attributes.
|
46
46
|
create_from_recordset: true, # Option for `recordset.create` vs `Model.create` behavior.
|
47
47
|
filter_recordset_before_find: true, # Option to control if filtering is done before find.
|
48
48
|
}.each do |a, default|
|
49
|
-
|
50
|
-
base.class_attribute(a)
|
49
|
+
next if base.respond_to?(a)
|
51
50
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
51
|
+
base.class_attribute(a)
|
52
|
+
|
53
|
+
# Set default manually so we can still support Rails 4. Maybe later we can use the default
|
54
|
+
# parameter on `class_attribute`.
|
55
|
+
base.send(:"#{a}=", default)
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
@@ -105,7 +105,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
105
105
|
|
106
106
|
# Helper to get the configured serializer class, or `NativeSerializer` as a default.
|
107
107
|
def get_serializer_class
|
108
|
-
return
|
108
|
+
return super || RESTFramework::NativeSerializer
|
109
109
|
end
|
110
110
|
|
111
111
|
# Helper to get filtering backends, defaulting to using `ModelFilter` and `ModelOrderingFilter`.
|
@@ -143,8 +143,8 @@ module RESTFramework::BaseModelControllerMixin
|
|
143
143
|
body_params
|
144
144
|
end
|
145
145
|
end
|
146
|
-
|
147
|
-
|
146
|
+
alias_method :get_create_params, :get_body_params
|
147
|
+
alias_method :get_update_params, :get_body_params
|
148
148
|
|
149
149
|
# Get the model for this controller.
|
150
150
|
def get_model(from_get_recordset: false)
|
@@ -160,7 +160,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
160
160
|
|
161
161
|
# Try to determine model from controller name.
|
162
162
|
begin
|
163
|
-
return
|
163
|
+
return @model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize
|
164
164
|
rescue NameError
|
165
165
|
end
|
166
166
|
|
@@ -196,48 +196,51 @@ module RESTFramework::BaseModelControllerMixin
|
|
196
196
|
recordset = self.get_filtered_data(recordset)
|
197
197
|
end
|
198
198
|
|
199
|
-
# Return the record.
|
200
|
-
|
201
|
-
return self.get_recordset.find_by!(find_by_key => find_by_value)
|
202
|
-
end
|
203
|
-
return nil
|
199
|
+
# Return the record. Route key is always :id by Rails convention.
|
200
|
+
return recordset.find_by!(find_by_key => params[:id])
|
204
201
|
end
|
205
202
|
end
|
206
203
|
|
207
|
-
|
208
204
|
# Mixin for listing records.
|
209
205
|
module RESTFramework::ListModelMixin
|
210
206
|
def index
|
207
|
+
api_response(self._index)
|
208
|
+
end
|
209
|
+
|
210
|
+
def _index
|
211
211
|
@records = self.get_filtered_data(self.get_recordset)
|
212
212
|
|
213
213
|
# Handle pagination, if enabled.
|
214
214
|
if self.class.paginator_class
|
215
215
|
paginator = self.class.paginator_class.new(data: @records, controller: self)
|
216
216
|
page = paginator.get_page
|
217
|
-
serialized_page = self.get_serializer_class.new(
|
218
|
-
|
217
|
+
serialized_page = self.get_serializer_class.new(page, controller: self).serialize
|
218
|
+
return paginator.get_paginated_response(serialized_page)
|
219
219
|
else
|
220
|
-
|
220
|
+
return self.get_serializer_class.new(@records, controller: self).serialize
|
221
221
|
end
|
222
|
-
|
223
|
-
return api_response(data)
|
224
222
|
end
|
225
223
|
end
|
226
224
|
|
227
|
-
|
228
225
|
# Mixin for showing records.
|
229
226
|
module RESTFramework::ShowModelMixin
|
230
227
|
def show
|
228
|
+
api_response(self._show)
|
229
|
+
end
|
230
|
+
|
231
|
+
def _show
|
231
232
|
@record = self.get_record
|
232
|
-
|
233
|
-
return api_response(serialized_record)
|
233
|
+
return self.get_serializer_class.new(@record, controller: self).serialize
|
234
234
|
end
|
235
235
|
end
|
236
236
|
|
237
|
-
|
238
237
|
# Mixin for creating records.
|
239
238
|
module RESTFramework::CreateModelMixin
|
240
239
|
def create
|
240
|
+
api_response(self._create)
|
241
|
+
end
|
242
|
+
|
243
|
+
def _create
|
241
244
|
if self.get_recordset.respond_to?(:create!) && self.create_from_recordset
|
242
245
|
# Create with any properties inherited from the recordset.
|
243
246
|
@record = self.get_recordset.create!(self.get_create_params)
|
@@ -245,39 +248,43 @@ module RESTFramework::CreateModelMixin
|
|
245
248
|
# Otherwise, perform a "bare" create.
|
246
249
|
@record = self.get_model.create!(self.get_create_params)
|
247
250
|
end
|
248
|
-
|
249
|
-
return
|
251
|
+
|
252
|
+
return self.get_serializer_class.new(@record, controller: self).serialize
|
250
253
|
end
|
251
254
|
end
|
252
255
|
|
253
|
-
|
254
256
|
# Mixin for updating records.
|
255
257
|
module RESTFramework::UpdateModelMixin
|
256
258
|
def update
|
259
|
+
api_response(self._update)
|
260
|
+
end
|
261
|
+
|
262
|
+
def _update
|
257
263
|
@record = self.get_record
|
258
264
|
@record.update!(self.get_update_params)
|
259
|
-
|
260
|
-
return api_response(serialized_record)
|
265
|
+
return self.get_serializer_class.new(@record, controller: self).serialize
|
261
266
|
end
|
262
267
|
end
|
263
268
|
|
264
|
-
|
265
269
|
# Mixin for destroying records.
|
266
270
|
module RESTFramework::DestroyModelMixin
|
267
271
|
def destroy
|
272
|
+
self._destroy
|
273
|
+
api_response("")
|
274
|
+
end
|
275
|
+
|
276
|
+
def _destroy
|
268
277
|
@record = self.get_record
|
269
278
|
@record.destroy!
|
270
|
-
api_response('')
|
271
279
|
end
|
272
280
|
end
|
273
281
|
|
274
|
-
|
275
282
|
# Mixin that includes show/list mixins.
|
276
283
|
module RESTFramework::ReadOnlyModelControllerMixin
|
277
284
|
include RESTFramework::BaseModelControllerMixin
|
278
285
|
|
279
286
|
def self.included(base)
|
280
|
-
if base.is_a?
|
287
|
+
if base.is_a?(Class)
|
281
288
|
RESTFramework::BaseModelControllerMixin.included(base)
|
282
289
|
end
|
283
290
|
end
|
@@ -286,13 +293,12 @@ module RESTFramework::ReadOnlyModelControllerMixin
|
|
286
293
|
include RESTFramework::ShowModelMixin
|
287
294
|
end
|
288
295
|
|
289
|
-
|
290
296
|
# Mixin that includes all the CRUD mixins.
|
291
297
|
module RESTFramework::ModelControllerMixin
|
292
298
|
include RESTFramework::BaseModelControllerMixin
|
293
299
|
|
294
300
|
def self.included(base)
|
295
|
-
if base.is_a?
|
301
|
+
if base.is_a?(Class)
|
296
302
|
RESTFramework::BaseModelControllerMixin.included(base)
|
297
303
|
end
|
298
304
|
end
|
@@ -2,10 +2,9 @@
|
|
2
2
|
class RESTFramework::Error < StandardError
|
3
3
|
end
|
4
4
|
|
5
|
-
|
6
5
|
class RESTFramework::NilPassedToAPIResponseError < RESTFramework::Error
|
7
6
|
def message
|
8
|
-
return <<~MSG.split("\n").join(
|
7
|
+
return <<~MSG.split("\n").join(" ")
|
9
8
|
Payload of `nil` was passed to `api_response`; this is unsupported. If you want a blank
|
10
9
|
response, pass `''` (an empty string) as the payload. If this was the result of a `find_by`
|
11
10
|
(or similar Active Record method) not finding a record, you should use the bang version (e.g.,
|
@@ -8,7 +8,6 @@ class RESTFramework::BaseFilter
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
|
12
11
|
# A simple filtering backend that supports filtering a recordset based on fields defined on the
|
13
12
|
# controller class.
|
14
13
|
class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
@@ -31,19 +30,19 @@ class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
|
31
30
|
end
|
32
31
|
end
|
33
32
|
|
34
|
-
|
35
33
|
# A filter backend which handles ordering of the recordset.
|
36
34
|
class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
37
35
|
# Convert ordering string to an ordering configuration.
|
38
36
|
def _get_ordering
|
39
37
|
return nil if @controller.class.ordering_query_param.blank?
|
38
|
+
|
40
39
|
ordering_fields = @controller.send(:get_ordering_fields)
|
41
40
|
order_string = @controller.params[@controller.class.ordering_query_param]
|
42
41
|
|
43
42
|
unless order_string.blank?
|
44
43
|
ordering = {}
|
45
|
-
order_string.split(
|
46
|
-
if field[0] ==
|
44
|
+
order_string.split(",").each do |field|
|
45
|
+
if field[0] == "-"
|
47
46
|
column = field[1..-1]
|
48
47
|
direction = :desc
|
49
48
|
else
|
@@ -63,7 +62,7 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
63
62
|
# Order data according to the request query parameters.
|
64
63
|
def get_filtered_data(data)
|
65
64
|
ordering = self._get_ordering
|
66
|
-
reorder = !@controller.
|
65
|
+
reorder = !@controller.class.ordering_no_reorder
|
67
66
|
|
68
67
|
if ordering && !ordering.empty?
|
69
68
|
return data.send(reorder ? :reorder : :order, _get_ordering)
|
@@ -73,19 +72,21 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
73
72
|
end
|
74
73
|
end
|
75
74
|
|
76
|
-
|
77
75
|
# Multi-field text searching on models.
|
78
76
|
class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
|
79
77
|
# Filter data according to the request query parameters.
|
80
78
|
def get_filtered_data(data)
|
81
79
|
fields = @controller.send(:get_search_fields)
|
82
|
-
search = @controller.request.query_parameters[@controller.
|
80
|
+
search = @controller.request.query_parameters[@controller.class.search_query_param]
|
83
81
|
|
84
82
|
# Ensure we use array conditions to prevent SQL injection.
|
85
83
|
unless search.blank?
|
86
|
-
return data.where(
|
87
|
-
|
88
|
-
|
84
|
+
return data.where(
|
85
|
+
fields.map { |f|
|
86
|
+
"CAST(#{f} AS CHAR) #{@controller.class.search_ilike ? "ILIKE" : "LIKE"} ?"
|
87
|
+
}.join(" OR "),
|
88
|
+
*(["%#{search}%"] * fields.length),
|
89
|
+
)
|
89
90
|
end
|
90
91
|
|
91
92
|
return data
|
@@ -1,7 +1,6 @@
|
|
1
|
-
require
|
1
|
+
require "rails/generators"
|
2
2
|
|
3
|
-
|
4
|
-
# Some projects don't have the inflection "REST" as an acronym, so this is a helper class to prevent
|
3
|
+
# Most projects don't have the inflection "REST" as an acronym, so this is a helper class to prevent
|
5
4
|
# this generator from being namespaced under `r_e_s_t_framework`.
|
6
5
|
# :nocov:
|
7
6
|
class RESTFrameworkCustomGeneratorControllerNamespace < String
|
@@ -11,18 +10,17 @@ class RESTFrameworkCustomGeneratorControllerNamespace < String
|
|
11
10
|
end
|
12
11
|
# :nocov:
|
13
12
|
|
14
|
-
|
15
13
|
class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
|
16
|
-
PATH_REGEX =
|
14
|
+
PATH_REGEX = %r{^[a-z0-9][a-z0-9_/]+$}
|
17
15
|
|
18
16
|
desc <<~END
|
19
|
-
|
17
|
+
Description:
|
20
18
|
Generates a new REST Framework controller.
|
21
19
|
|
22
20
|
Specify the controller as a path, including the module, if needed, like:
|
23
21
|
'parent_module/controller_name'.
|
24
22
|
|
25
|
-
|
23
|
+
Example:
|
26
24
|
`rails generate rest_framework:controller user_api/groups`
|
27
25
|
|
28
26
|
Generates a controller at `app/controllers/user_api/groups_controller.rb` named
|
@@ -31,10 +29,7 @@ class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
|
|
31
29
|
|
32
30
|
argument :path, type: :string
|
33
31
|
class_option(
|
34
|
-
:parent_class,
|
35
|
-
type: :string,
|
36
|
-
default: 'ApplicationController',
|
37
|
-
desc: "Inheritance parent",
|
32
|
+
:parent_class, type: :string, default: "ApplicationController", desc: "Inheritance parent"
|
38
33
|
)
|
39
34
|
class_option(
|
40
35
|
:include_base,
|
@@ -50,11 +45,13 @@ class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
|
|
50
45
|
end
|
51
46
|
|
52
47
|
def create_rest_controller_file
|
53
|
-
unless
|
54
|
-
raise StandardError
|
48
|
+
unless PATH_REGEX.match?(self.path)
|
49
|
+
raise StandardError, "Path isn't valid."
|
55
50
|
end
|
56
51
|
|
57
|
-
|
52
|
+
# Remove '_controller' from end of path, if it exists.
|
53
|
+
cleaned_path = self.path.delete_suffix("_controller")
|
54
|
+
|
58
55
|
content = <<~END
|
59
56
|
class #{cleaned_path.camelize}Controller < #{options[:parent_class]}
|
60
57
|
include RESTFramework::#{
|
@@ -62,6 +59,6 @@ class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
|
|
62
59
|
}
|
63
60
|
end
|
64
61
|
END
|
65
|
-
create_file("app/controllers/#{
|
62
|
+
create_file("app/controllers/#{cleaned_path}_controller.rb", content)
|
66
63
|
end
|
67
64
|
end
|
@@ -15,7 +15,6 @@ class RESTFramework::BasePaginator
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
|
19
18
|
# A simple paginator based on page numbers.
|
20
19
|
#
|
21
20
|
# Example: http://example.com/api/users/?page=3&page_size=50
|
@@ -26,7 +25,7 @@ class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
|
|
26
25
|
@page_size = self._page_size
|
27
26
|
|
28
27
|
@total_pages = @count / @page_size
|
29
|
-
@total_pages += 1 if
|
28
|
+
@total_pages += 1 if @count % @page_size != 0
|
30
29
|
end
|
31
30
|
|
32
31
|
def _page_size
|
@@ -60,7 +59,7 @@ class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
|
|
60
59
|
# Get the page and return it so the caller can serialize it.
|
61
60
|
def get_page(page_number=nil)
|
62
61
|
# If page number isn't provided, infer from the params or use 1 as a fallback value.
|
63
|
-
|
62
|
+
unless page_number
|
64
63
|
page_number = @controller&.params&.[](self._page_query_param)
|
65
64
|
if page_number.blank?
|
66
65
|
page_number = 1
|
@@ -90,7 +89,6 @@ class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
|
|
90
89
|
end
|
91
90
|
end
|
92
91
|
|
93
|
-
|
94
92
|
# TODO: implement this
|
95
93
|
# class RESTFramework::CountOffsetPaginator
|
96
94
|
# end
|
@@ -1,11 +1,11 @@
|
|
1
|
-
require
|
2
|
-
require_relative
|
1
|
+
require "action_dispatch/routing/mapper"
|
2
|
+
require_relative "utils"
|
3
3
|
|
4
4
|
module ActionDispatch::Routing
|
5
5
|
class Mapper
|
6
6
|
# Internal interface to get the controller class from the name and current scope.
|
7
7
|
protected def _get_controller_class(name, pluralize: true, fallback_reverse_pluralization: true)
|
8
|
-
#
|
8
|
+
# Get class name.
|
9
9
|
name = name.to_s.camelize # camelize to leave plural names plural
|
10
10
|
name = name.pluralize if pluralize
|
11
11
|
if name == name.pluralize
|
@@ -16,14 +16,14 @@ module ActionDispatch::Routing
|
|
16
16
|
name += "Controller"
|
17
17
|
name_reverse += "Controller"
|
18
18
|
|
19
|
-
#
|
19
|
+
# Get scope for the class.
|
20
20
|
if @scope[:module]
|
21
|
-
mod = @scope[:module].to_s.
|
21
|
+
mod = @scope[:module].to_s.camelize.constantize
|
22
22
|
else
|
23
23
|
mod = Object
|
24
24
|
end
|
25
25
|
|
26
|
-
#
|
26
|
+
# Convert class name to class.
|
27
27
|
begin
|
28
28
|
controller = mod.const_get(name)
|
29
29
|
rescue NameError
|
@@ -83,15 +83,15 @@ module ActionDispatch::Routing
|
|
83
83
|
public_send(resource_method, name, except: skip, **kwargs) do
|
84
84
|
if controller_class.respond_to?(:extra_member_actions)
|
85
85
|
member do
|
86
|
-
actions = RESTFramework::Utils
|
87
|
-
controller_class.extra_member_actions
|
86
|
+
actions = RESTFramework::Utils.parse_extra_actions(
|
87
|
+
controller_class.extra_member_actions,
|
88
88
|
)
|
89
89
|
self._route_extra_actions(actions)
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
93
93
|
collection do
|
94
|
-
actions = RESTFramework::Utils
|
94
|
+
actions = RESTFramework::Utils.parse_extra_actions(controller_class.extra_actions)
|
95
95
|
self._route_extra_actions(actions)
|
96
96
|
end
|
97
97
|
|
@@ -127,11 +127,11 @@ module ActionDispatch::Routing
|
|
127
127
|
kwargs[:controller] = name unless kwargs[:controller]
|
128
128
|
|
129
129
|
# Route actions using the resourceful router, but skip all builtin actions.
|
130
|
-
actions = RESTFramework::Utils
|
130
|
+
actions = RESTFramework::Utils.parse_extra_actions(controller_class.extra_actions)
|
131
131
|
public_send(:resource, name, only: [], **kwargs) do
|
132
132
|
# Route a root for this resource.
|
133
133
|
if route_root_to
|
134
|
-
get
|
134
|
+
get("", action: route_root_to)
|
135
135
|
end
|
136
136
|
|
137
137
|
self._route_extra_actions(actions, &block)
|
@@ -146,13 +146,12 @@ module ActionDispatch::Routing
|
|
146
146
|
|
147
147
|
# Remove path if name is nil (routing to the root of current namespace).
|
148
148
|
unless name
|
149
|
-
kwargs[:path] =
|
149
|
+
kwargs[:path] = ""
|
150
150
|
end
|
151
151
|
|
152
152
|
return rest_route(controller, route_root_to: root_action, **kwargs) do
|
153
153
|
yield if block_given?
|
154
154
|
end
|
155
155
|
end
|
156
|
-
|
157
156
|
end
|
158
157
|
end
|
@@ -1,14 +1,23 @@
|
|
1
|
+
# The base serializer defines the interface for all REST Framework serializers.
|
1
2
|
class RESTFramework::BaseSerializer
|
2
|
-
|
3
|
+
attr_accessor :object
|
4
|
+
|
5
|
+
def initialize(object=nil, controller: nil, **kwargs)
|
3
6
|
@object = object
|
4
7
|
@controller = controller
|
5
8
|
end
|
6
9
|
|
7
|
-
|
10
|
+
# The primary interface for extracting a native Ruby types. This works both for records and
|
11
|
+
# collections.
|
12
|
+
def serialize(**kwargs)
|
8
13
|
raise NotImplementedError
|
9
14
|
end
|
10
|
-
end
|
11
15
|
|
16
|
+
# Synonym for `serializable_hash` or compatibility with ActiveModelSerializers.
|
17
|
+
def serializable_hash(**kwargs)
|
18
|
+
return self.serialize(**kwargs)
|
19
|
+
end
|
20
|
+
end
|
12
21
|
|
13
22
|
# This serializer uses `.serializable_hash` to convert objects to Ruby primitives (with the
|
14
23
|
# top-level being either an array or a hash).
|
@@ -18,8 +27,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
18
27
|
class_attribute :plural_config
|
19
28
|
class_attribute :action_config
|
20
29
|
|
21
|
-
def initialize(many: nil, model: nil, **kwargs)
|
22
|
-
super(**kwargs)
|
30
|
+
def initialize(object=nil, many: nil, model: nil, **kwargs)
|
31
|
+
super(object, **kwargs)
|
23
32
|
|
24
33
|
if many.nil?
|
25
34
|
# Determine if we are dealing with many objects or just one.
|
@@ -31,9 +40,9 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
31
40
|
# Determine model either explicitly, or by inspecting @object or @controller.
|
32
41
|
@model = model
|
33
42
|
@model ||= @object.class if @object.is_a?(ActiveRecord::Base)
|
34
|
-
@model ||= @object[0].class if
|
43
|
+
@model ||= @object[0].class if
|
35
44
|
@many && @object.is_a?(Enumerable) && @object.is_a?(ActiveRecord::Base)
|
36
|
-
|
45
|
+
|
37
46
|
@model ||= @controller.send(:get_model) if @controller
|
38
47
|
end
|
39
48
|
|
@@ -66,23 +75,77 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
66
75
|
return nil unless @controller
|
67
76
|
|
68
77
|
if @many == true
|
69
|
-
controller_serializer = @controller.try(:native_serializer_plural_config)
|
78
|
+
controller_serializer = @controller.class.try(:native_serializer_plural_config)
|
70
79
|
elsif @many == false
|
71
|
-
controller_serializer = @controller.try(:native_serializer_singular_config)
|
80
|
+
controller_serializer = @controller.class.try(:native_serializer_singular_config)
|
72
81
|
end
|
73
82
|
|
74
|
-
return controller_serializer || @controller.try(:native_serializer_config)
|
83
|
+
return controller_serializer || @controller.class.try(:native_serializer_config)
|
75
84
|
end
|
76
85
|
|
77
|
-
#
|
78
|
-
def
|
86
|
+
# Helper to filter (mutate) a single subconfig for specific keys.
|
87
|
+
def self.filter_subconfig(subconfig, except, additive: false)
|
88
|
+
return subconfig if !subconfig && !additive
|
89
|
+
|
90
|
+
if subconfig.is_a?(Array)
|
91
|
+
subconfig = subconfig.map(&:to_sym)
|
92
|
+
if additive
|
93
|
+
# Only add fields which are not already included.
|
94
|
+
subconfig += except - subconfig
|
95
|
+
else
|
96
|
+
subconfig -= except
|
97
|
+
end
|
98
|
+
elsif subconfig.is_a?(Hash)
|
99
|
+
# Additive doesn't make sense in a hash context since we wouldn't know the values.
|
100
|
+
unless additive
|
101
|
+
subconfig.symbolize_keys!
|
102
|
+
subconfig.reject! { |k, _v| k.in?(except) }
|
103
|
+
end
|
104
|
+
elsif !subconfig
|
105
|
+
else # Subconfig is a single element (assume string/symbol).
|
106
|
+
subconfig = subconfig.to_sym
|
107
|
+
if subconfig.in?(except)
|
108
|
+
subconfig = [] unless additive
|
109
|
+
elsif additive
|
110
|
+
subconfig = [subconfig, *except]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
return subconfig
|
115
|
+
end
|
116
|
+
|
117
|
+
# Helper to filter out configuration properties based on the :except query parameter.
|
118
|
+
def filter_except(config)
|
119
|
+
return config unless @controller
|
120
|
+
|
121
|
+
except_query_param = @controller.class.try(:native_serializer_except_query_param)
|
122
|
+
if except = @controller.request.query_parameters[except_query_param].presence
|
123
|
+
except = except.split(",").map(&:strip).map(&:to_sym)
|
124
|
+
|
125
|
+
unless except.empty?
|
126
|
+
# Duplicate the config to avoid mutating class state.
|
127
|
+
config = config.deep_dup
|
128
|
+
|
129
|
+
# Filter `only`, `except` (additive), `include`, and `methods`.
|
130
|
+
config[:only] = self.class.filter_subconfig(config[:only], except)
|
131
|
+
config[:except] = self.class.filter_subconfig(config[:except], except, additive: true)
|
132
|
+
config[:include] = self.class.filter_subconfig(config[:include], except)
|
133
|
+
config[:methods] = self.class.filter_subconfig(config[:methods], except)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
return config
|
138
|
+
end
|
139
|
+
|
140
|
+
# Get the raw serializer config.
|
141
|
+
def _get_raw_serializer_config
|
79
142
|
# Return a locally defined serializer config if one is defined.
|
80
143
|
if local_config = self.get_local_native_serializer_config
|
81
144
|
return local_config
|
82
145
|
end
|
83
146
|
|
84
147
|
# Return a serializer config if one is defined on the controller.
|
85
|
-
if serializer_config = get_controller_native_serializer_config
|
148
|
+
if serializer_config = self.get_controller_native_serializer_config
|
86
149
|
return serializer_config
|
87
150
|
end
|
88
151
|
|
@@ -103,13 +166,16 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
103
166
|
return {}
|
104
167
|
end
|
105
168
|
|
106
|
-
#
|
107
|
-
def
|
108
|
-
|
169
|
+
# Get a configuration passable to `serializable_hash` for the object, filtered if required.
|
170
|
+
def get_serializer_config
|
171
|
+
return filter_except(self._get_raw_serializer_config)
|
172
|
+
end
|
109
173
|
|
110
|
-
|
174
|
+
def serialize(**kwargs)
|
175
|
+
if @object.respond_to?(:to_ary)
|
111
176
|
return @object.map { |r| r.serializable_hash(self.get_serializer_config) }
|
112
177
|
end
|
178
|
+
|
113
179
|
return @object.serializable_hash(self.get_serializer_config)
|
114
180
|
end
|
115
181
|
|
@@ -118,6 +184,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
118
184
|
@_nested_config ||= self.get_serializer_config
|
119
185
|
return @_nested_config[key]
|
120
186
|
end
|
187
|
+
|
121
188
|
def []=(key, value)
|
122
189
|
@_nested_config ||= self.get_serializer_config
|
123
190
|
return @_nested_config[key] = value
|
@@ -128,20 +195,20 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
128
195
|
@_nested_config ||= self.new.get_serializer_config
|
129
196
|
return @_nested_config[key]
|
130
197
|
end
|
198
|
+
|
131
199
|
def self.[]=(key, value)
|
132
200
|
@_nested_config ||= self.new.get_serializer_config
|
133
201
|
return @_nested_config[key] = value
|
134
202
|
end
|
135
203
|
end
|
136
204
|
|
137
|
-
|
138
205
|
# :nocov:
|
139
206
|
# Alias NativeModelSerializer -> NativeSerializer.
|
140
207
|
class RESTFramework::NativeModelSerializer < RESTFramework::NativeSerializer
|
141
208
|
def initialize(**kwargs)
|
142
209
|
super
|
143
210
|
ActiveSupport::Deprecation.warn(
|
144
|
-
<<~MSG.split("\n").join(
|
211
|
+
<<~MSG.split("\n").join(" "),
|
145
212
|
RESTFramework::NativeModelSerializer is deprecated and will be removed in future versions of
|
146
213
|
REST Framework; you should use RESTFramework::NativeSerializer instead.
|
147
214
|
MSG
|
@@ -149,3 +216,19 @@ class RESTFramework::NativeModelSerializer < RESTFramework::NativeSerializer
|
|
149
216
|
end
|
150
217
|
end
|
151
218
|
# :nocov:
|
219
|
+
|
220
|
+
# This is a helper factory to wrap an ActiveModelSerializer to provide a `serialize` method which
|
221
|
+
# accepts both collections and individual records. Use `.for` to build adapters.
|
222
|
+
class RESTFramework::ActiveModelSerializerAdapterFactory
|
223
|
+
def self.for(active_model_serializer)
|
224
|
+
return Class.new(active_model_serializer) do
|
225
|
+
def serialize
|
226
|
+
if self.object.respond_to?(:to_ary)
|
227
|
+
return self.object.map { |r| self.class.superclass.new(r).serializable_hash }
|
228
|
+
end
|
229
|
+
|
230
|
+
return self.serializable_hash
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
data/lib/rest_framework/utils.rb
CHANGED
@@ -2,7 +2,7 @@ module RESTFramework::Utils
|
|
2
2
|
# Helper to take extra_actions hash and convert to a consistent format:
|
3
3
|
# `{paths:, methods:, kwargs:}`.
|
4
4
|
def self.parse_extra_actions(extra_actions)
|
5
|
-
return (extra_actions || {}).map do |k,v|
|
5
|
+
return (extra_actions || {}).map do |k, v|
|
6
6
|
kwargs = {action: k}
|
7
7
|
path = k
|
8
8
|
|
@@ -38,7 +38,7 @@ module RESTFramework::Utils
|
|
38
38
|
# Helper to get the current route pattern, stripped of the `(:format)` segment.
|
39
39
|
def self.get_route_pattern(application_routes, request)
|
40
40
|
application_routes.router.recognize(request) do |route, _, _|
|
41
|
-
return route.path.spec.to_s.gsub(/\(\.:format\)$/,
|
41
|
+
return route.path.spec.to_s.gsub(/\(\.:format\)$/, "")
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
@@ -18,7 +18,7 @@ module RESTFramework
|
|
18
18
|
end
|
19
19
|
|
20
20
|
# No VERSION file, so version is unknown.
|
21
|
-
return
|
21
|
+
return "unknown"
|
22
22
|
end
|
23
23
|
|
24
24
|
def self.stamp_version
|
@@ -30,5 +30,5 @@ module RESTFramework
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
-
VERSION = Version.get_version
|
33
|
+
VERSION = Version.get_version
|
34
34
|
end
|
data/lib/rest_framework.rb
CHANGED
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.
|
4
|
+
version: 0.5.2
|
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:
|
11
|
+
date: 2022-03-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -74,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0'
|
76
76
|
requirements: []
|
77
|
-
rubygems_version: 3.
|
77
|
+
rubygems_version: 3.2.22
|
78
78
|
signing_key:
|
79
79
|
specification_version: 4
|
80
80
|
summary: A framework for DRY RESTful APIs in Ruby on Rails.
|