rest_framework 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +11 -12
- data/lib/rest_framework/serializers.rb +91 -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: 9791b0cbb0a806afa00955abf151c7d04bd1820bdbe189f237badc9782f87d6b
|
4
|
+
data.tar.gz: 69e29341f8804fac7894c0065b0f5535d88b0e8d60577bc77677fe13d633f2d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f04bf8059e765db9e13a9729db43c2b26a914035063fe728e9ad0bcb1ab968bb8939a4d278e239a39f64b671d183bfdd7a2487248495cd7e5bd26b93ac49a103
|
7
|
+
data.tar.gz: '0908d43c8b2188e4dde5920a84eb621c0dea2dc170891e314d77d95b823a32af6681ae9a6028277f32282b88be03f8e95da55ec310f75e3c89ec8c3a09934f09'
|
data/LICENSE
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.0
|
@@ -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
21
|
mod = @scope[:module].to_s.classify.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,66 @@ 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 unless subconfig
|
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
|
+
subconfig.symbolize_keys!
|
100
|
+
subconfig.reject! { |k, _v| k.in?(except) }
|
101
|
+
end
|
102
|
+
|
103
|
+
return subconfig
|
104
|
+
end
|
105
|
+
|
106
|
+
# Helper to filter out configuration properties based on the :except query parameter.
|
107
|
+
def filter_except(config)
|
108
|
+
return config unless @controller
|
109
|
+
|
110
|
+
except_query_param = @controller.class.try(:native_serializer_except_query_param)
|
111
|
+
if except = @controller.request.query_parameters[except_query_param]
|
112
|
+
except = except.split(",").map(&:strip).map(&:to_sym)
|
113
|
+
|
114
|
+
unless except.empty?
|
115
|
+
# Duplicate the config to avoid mutating class state.
|
116
|
+
config = config.deep_dup
|
117
|
+
|
118
|
+
# Filter `only`, `except` (additive), `include`, and `methods`.
|
119
|
+
self.class.filter_subconfig(config[:only], except)
|
120
|
+
self.class.filter_subconfig(config[:except], except, additive: true)
|
121
|
+
self.class.filter_subconfig(config[:include], except)
|
122
|
+
self.class.filter_subconfig(config[:methods], except)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
return config
|
127
|
+
end
|
128
|
+
|
129
|
+
# Get the raw serializer config.
|
130
|
+
def _get_raw_serializer_config
|
79
131
|
# Return a locally defined serializer config if one is defined.
|
80
132
|
if local_config = self.get_local_native_serializer_config
|
81
133
|
return local_config
|
82
134
|
end
|
83
135
|
|
84
136
|
# Return a serializer config if one is defined on the controller.
|
85
|
-
if serializer_config = get_controller_native_serializer_config
|
137
|
+
if serializer_config = self.get_controller_native_serializer_config
|
86
138
|
return serializer_config
|
87
139
|
end
|
88
140
|
|
@@ -103,13 +155,16 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
103
155
|
return {}
|
104
156
|
end
|
105
157
|
|
106
|
-
#
|
107
|
-
def
|
108
|
-
|
158
|
+
# Get a configuration passable to `serializable_hash` for the object, filtered if required.
|
159
|
+
def get_serializer_config
|
160
|
+
return filter_except(self._get_raw_serializer_config)
|
161
|
+
end
|
109
162
|
|
110
|
-
|
163
|
+
def serialize(**kwargs)
|
164
|
+
if @object.respond_to?(:to_ary)
|
111
165
|
return @object.map { |r| r.serializable_hash(self.get_serializer_config) }
|
112
166
|
end
|
167
|
+
|
113
168
|
return @object.serializable_hash(self.get_serializer_config)
|
114
169
|
end
|
115
170
|
|
@@ -118,6 +173,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
118
173
|
@_nested_config ||= self.get_serializer_config
|
119
174
|
return @_nested_config[key]
|
120
175
|
end
|
176
|
+
|
121
177
|
def []=(key, value)
|
122
178
|
@_nested_config ||= self.get_serializer_config
|
123
179
|
return @_nested_config[key] = value
|
@@ -128,20 +184,20 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
128
184
|
@_nested_config ||= self.new.get_serializer_config
|
129
185
|
return @_nested_config[key]
|
130
186
|
end
|
187
|
+
|
131
188
|
def self.[]=(key, value)
|
132
189
|
@_nested_config ||= self.new.get_serializer_config
|
133
190
|
return @_nested_config[key] = value
|
134
191
|
end
|
135
192
|
end
|
136
193
|
|
137
|
-
|
138
194
|
# :nocov:
|
139
195
|
# Alias NativeModelSerializer -> NativeSerializer.
|
140
196
|
class RESTFramework::NativeModelSerializer < RESTFramework::NativeSerializer
|
141
197
|
def initialize(**kwargs)
|
142
198
|
super
|
143
199
|
ActiveSupport::Deprecation.warn(
|
144
|
-
<<~MSG.split("\n").join(
|
200
|
+
<<~MSG.split("\n").join(" "),
|
145
201
|
RESTFramework::NativeModelSerializer is deprecated and will be removed in future versions of
|
146
202
|
REST Framework; you should use RESTFramework::NativeSerializer instead.
|
147
203
|
MSG
|
@@ -149,3 +205,19 @@ class RESTFramework::NativeModelSerializer < RESTFramework::NativeSerializer
|
|
149
205
|
end
|
150
206
|
end
|
151
207
|
# :nocov:
|
208
|
+
|
209
|
+
# This is a helper factory to wrap an ActiveModelSerializer to provide a `serialize` method which
|
210
|
+
# accepts both collections and individual records. Use `.for` to build adapters.
|
211
|
+
class RESTFramework::ActiveModelSerializerAdapterFactory
|
212
|
+
def self.for(active_model_serializer)
|
213
|
+
return Class.new(active_model_serializer) do
|
214
|
+
def serialize
|
215
|
+
if self.object.respond_to?(:to_ary)
|
216
|
+
return self.object.map { |r| self.class.superclass.new(r).serializable_hash }
|
217
|
+
end
|
218
|
+
|
219
|
+
return self.serializable_hash
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
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.0
|
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-05 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.
|