rest_framework 1.0.0.beta2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +32 -24
- data/VERSION +1 -1
- data/lib/rest_framework/engine.rb +6 -0
- data/lib/rest_framework/errors/nil_passed_to_render_api_error.rb +1 -1
- data/lib/rest_framework/errors/unknown_model_error.rb +1 -1
- data/lib/rest_framework/filters/ordering_filter.rb +3 -3
- data/lib/rest_framework/filters/query_filter.rb +101 -33
- data/lib/rest_framework/filters/ransack_filter.rb +1 -1
- data/lib/rest_framework/filters/search_filter.rb +3 -3
- data/lib/rest_framework/generators/controller_generator.rb +2 -2
- data/lib/rest_framework/mixins/base_controller_mixin.rb +90 -51
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +12 -19
- data/lib/rest_framework/mixins/model_controller_mixin.rb +95 -56
- data/lib/rest_framework/paginators/page_number_paginator.rb +6 -6
- data/lib/rest_framework/routers.rb +16 -9
- data/lib/rest_framework/serializers/active_model_serializer_adapter_factory.rb +4 -2
- data/lib/rest_framework/serializers/base_serializer.rb +7 -5
- data/lib/rest_framework/serializers/native_serializer.rb +22 -22
- data/lib/rest_framework/utils.rb +33 -47
- data/lib/rest_framework/version.rb +1 -1
- data/lib/rest_framework.rb +26 -15
- metadata +5 -5
@@ -11,7 +11,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
11
11
|
title: nil,
|
12
12
|
description: nil,
|
13
13
|
version: nil,
|
14
|
-
inflect_acronyms: ["ID", "IDs", "REST", "API", "APIs"].freeze,
|
14
|
+
inflect_acronyms: [ "ID", "IDs", "REST", "API", "APIs" ].freeze,
|
15
15
|
openapi_include_children: false,
|
16
16
|
|
17
17
|
# Options related to serialization.
|
@@ -38,14 +38,14 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
38
38
|
|
39
39
|
# Default action for API root.
|
40
40
|
def root
|
41
|
-
|
41
|
+
render(api: { message: "This is the API root." })
|
42
42
|
end
|
43
43
|
|
44
44
|
module ClassMethods
|
45
45
|
# By default, this is the name of the controller class, titleized and with any custom inflection
|
46
46
|
# acronyms applied.
|
47
47
|
def get_title
|
48
|
-
|
48
|
+
self.title || RESTFramework::Utils.inflect(
|
49
49
|
self.name.demodulize.chomp("Controller").titleize(keep_id_suffix: true),
|
50
50
|
self.inflect_acronyms,
|
51
51
|
)
|
@@ -53,10 +53,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
53
53
|
|
54
54
|
# Get a label from a field/column name, titleized and inflected.
|
55
55
|
def label_for(s)
|
56
|
-
|
57
|
-
s.to_s.titleize(keep_id_suffix: true),
|
58
|
-
self.inflect_acronyms,
|
59
|
-
)
|
56
|
+
RESTFramework::Utils.inflect(s.to_s.titleize(keep_id_suffix: true), self.inflect_acronyms)
|
60
57
|
end
|
61
58
|
|
62
59
|
# Define any behavior to execute at the end of controller definition.
|
@@ -71,50 +68,58 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
71
68
|
end
|
72
69
|
# :nocov:
|
73
70
|
|
74
|
-
def
|
75
|
-
|
71
|
+
def openapi_response_content_types
|
72
|
+
@openapi_response_content_types ||= [
|
76
73
|
"text/html",
|
77
74
|
self.serialize_to_json ? "application/json" : nil,
|
78
75
|
self.serialize_to_xml ? "application/xml" : nil,
|
79
76
|
].compact
|
80
|
-
|
77
|
+
end
|
78
|
+
|
79
|
+
def openapi_request_content_types
|
80
|
+
@openapi_request_content_types ||= [
|
81
81
|
"application/json",
|
82
|
-
"application/xml",
|
83
82
|
"application/x-www-form-urlencoded",
|
84
83
|
"multipart/form-data",
|
85
84
|
]
|
85
|
+
end
|
86
|
+
|
87
|
+
def openapi_paths(routes, tag)
|
88
|
+
resp_cts = self.openapi_response_content_types
|
89
|
+
req_cts = self.openapi_request_content_types
|
86
90
|
|
87
|
-
|
91
|
+
routes.group_by { |r| r[:concat_path] }.map { |concat_path, routes|
|
88
92
|
[
|
89
93
|
concat_path.gsub(/:([0-9A-Za-z_-]+)/, "{\\1}"),
|
90
94
|
routes.map { |route|
|
91
|
-
metadata =
|
92
|
-
summary = metadata
|
93
|
-
description = metadata
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
95
|
+
metadata = RESTFramework::ROUTE_METADATA[route[:path]] || {}
|
96
|
+
summary = metadata.delete(:label).presence || self.label_for(route[:action])
|
97
|
+
description = metadata.delete(:description).presence
|
98
|
+
extra_action = RESTFramework::EXTRA_ACTION_ROUTES.include?(route[:path])
|
99
|
+
error_response = { "$ref" => "#/components/responses/BadRequest" }
|
100
|
+
not_found_response = { "$ref" => "#/components/responses/NotFound" }
|
101
|
+
spec = { tags: [ tag ], summary: summary, description: description }.compact
|
102
|
+
|
103
|
+
# All routes should have a successful response.
|
104
|
+
spec[:responses] = {
|
105
|
+
200 => { content: resp_cts.map { |ct| [ ct, {} ] }.to_h, description: "Success" },
|
106
|
+
}
|
107
|
+
|
108
|
+
# Builtin POST, PUT, PATCH, and DELETE should have a 400 and 404 response.
|
109
|
+
if route[:verb].in?([ "POST", "PUT", "PATCH", "DELETE" ]) && !extra_action
|
110
|
+
spec[:responses][400] = error_response
|
111
|
+
spec[:responses][404] = not_found_response
|
112
|
+
end
|
113
|
+
|
114
|
+
# All POST, PUT, PATCH should have a request body.
|
115
|
+
if route[:verb].in?([ "POST", "PUT", "PATCH" ])
|
116
|
+
spec[:requestBody] ||= { content: req_cts.map { |ct| [ ct, {} ] }.to_h }
|
117
|
+
end
|
118
|
+
|
119
|
+
# Add remaining metadata as an extension.
|
120
|
+
spec["x-rrf-metadata"] = metadata if metadata.present?
|
121
|
+
|
122
|
+
next route[:verb].downcase, spec
|
118
123
|
}.to_h.merge(
|
119
124
|
{
|
120
125
|
parameters: routes.first[:route].required_parts.map { |p|
|
@@ -122,7 +127,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
122
127
|
name: p,
|
123
128
|
in: "path",
|
124
129
|
required: true,
|
125
|
-
schema: {type: "integer"},
|
130
|
+
schema: { type: "integer" },
|
126
131
|
}
|
127
132
|
},
|
128
133
|
},
|
@@ -134,16 +139,49 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
134
139
|
def openapi_document(request, route_group_name, routes)
|
135
140
|
server = request.base_url + request.original_fullpath.gsub(/\?.*/, "")
|
136
141
|
|
137
|
-
|
142
|
+
{
|
138
143
|
openapi: "3.1.1",
|
139
144
|
info: {
|
140
145
|
title: self.get_title,
|
141
146
|
description: self.description,
|
142
147
|
version: self.version.to_s,
|
143
148
|
}.compact,
|
144
|
-
servers: [{url: server}],
|
149
|
+
servers: [ { url: server } ],
|
145
150
|
paths: self.openapi_paths(routes, route_group_name),
|
146
|
-
tags: [{name: route_group_name, description: self.description}.compact],
|
151
|
+
tags: [ { name: route_group_name, description: self.description }.compact ],
|
152
|
+
components: {
|
153
|
+
schemas: {
|
154
|
+
"Error" => {
|
155
|
+
type: "object",
|
156
|
+
required: [ "message" ],
|
157
|
+
properties: {
|
158
|
+
message: { type: "string" },
|
159
|
+
errors: { type: "object" },
|
160
|
+
exception: { type: "string" },
|
161
|
+
},
|
162
|
+
},
|
163
|
+
},
|
164
|
+
responses: {
|
165
|
+
"BadRequest": {
|
166
|
+
description: "Bad Request",
|
167
|
+
content: self.openapi_response_content_types.map { |ct|
|
168
|
+
[
|
169
|
+
ct,
|
170
|
+
ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } },
|
171
|
+
]
|
172
|
+
}.to_h,
|
173
|
+
},
|
174
|
+
"NotFound": {
|
175
|
+
description: "Not Found",
|
176
|
+
content: self.openapi_response_content_types.map { |ct|
|
177
|
+
[
|
178
|
+
ct,
|
179
|
+
ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } },
|
180
|
+
]
|
181
|
+
}.to_h,
|
182
|
+
},
|
183
|
+
},
|
184
|
+
},
|
147
185
|
}.compact
|
148
186
|
end
|
149
187
|
end
|
@@ -182,6 +220,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
182
220
|
base.rescue_from(
|
183
221
|
ActionController::ParameterMissing,
|
184
222
|
ActionController::UnpermittedParameters,
|
223
|
+
ActionDispatch::Http::Parameters::ParseError,
|
185
224
|
ActiveRecord::AssociationTypeMismatch,
|
186
225
|
ActiveRecord::NotNullViolation,
|
187
226
|
ActiveRecord::RecordNotFound,
|
@@ -195,7 +234,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
195
234
|
end
|
196
235
|
|
197
236
|
# Use `TracePoint` hook to automatically call `rrf_finalize`.
|
198
|
-
|
237
|
+
if RESTFramework.config.auto_finalize
|
199
238
|
# :nocov:
|
200
239
|
TracePoint.trace(:end) do |t|
|
201
240
|
next if base != t.self
|
@@ -211,12 +250,12 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
211
250
|
end
|
212
251
|
|
213
252
|
def get_serializer_class
|
214
|
-
|
253
|
+
self.class.serializer_class
|
215
254
|
end
|
216
255
|
|
217
256
|
# Serialize the given data using the `serializer_class`.
|
218
257
|
def serialize(data, **kwargs)
|
219
|
-
|
258
|
+
RESTFramework::Utils.wrap_ams(self.get_serializer_class).new(
|
220
259
|
data, controller: self, **kwargs
|
221
260
|
).serialize
|
222
261
|
end
|
@@ -229,8 +268,8 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
229
268
|
400
|
230
269
|
end
|
231
270
|
|
232
|
-
|
233
|
-
{
|
271
|
+
render(
|
272
|
+
api: {
|
234
273
|
message: e.message,
|
235
274
|
errors: e.try(:record).try(:errors),
|
236
275
|
exception: RESTFramework.config.show_backtrace ? e.full_message : nil,
|
@@ -240,7 +279,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
240
279
|
end
|
241
280
|
|
242
281
|
def route_groups
|
243
|
-
|
282
|
+
@route_groups ||= RESTFramework::Utils.get_routes(Rails.application.routes, request)
|
244
283
|
end
|
245
284
|
|
246
285
|
# Render a browsable API for `html` format, along with basic `json`/`xml` formats, and with
|
@@ -341,11 +380,11 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
341
380
|
end
|
342
381
|
end
|
343
382
|
|
344
|
-
|
383
|
+
document
|
345
384
|
end
|
346
385
|
|
347
386
|
def options
|
348
|
-
|
387
|
+
render(api: self.openapi_document)
|
349
388
|
end
|
350
389
|
end
|
351
390
|
|
@@ -7,17 +7,17 @@ module RESTFramework::Mixins::BulkCreateModelMixin
|
|
7
7
|
# overloads the existing collection `POST` endpoint, so we add a special key to the OpenAPI
|
8
8
|
# metadata to indicate bulk create is supported.
|
9
9
|
def openapi_document
|
10
|
-
|
10
|
+
super.merge({ "x-rrf-bulk-create": true })
|
11
11
|
end
|
12
12
|
|
13
13
|
def create
|
14
14
|
if params[:_json].is_a?(Array)
|
15
15
|
records = self.create_all!
|
16
16
|
serialized_records = self.bulk_serialize(records)
|
17
|
-
return
|
17
|
+
return render(api: serialized_records)
|
18
18
|
end
|
19
19
|
|
20
|
-
|
20
|
+
super
|
21
21
|
end
|
22
22
|
|
23
23
|
# Perform the `create` call, and return the collection of (possibly) created records.
|
@@ -25,9 +25,7 @@ module RESTFramework::Mixins::BulkCreateModelMixin
|
|
25
25
|
create_data = self.get_create_params(bulk_mode: true)[:_json]
|
26
26
|
|
27
27
|
# Perform bulk create in a transaction.
|
28
|
-
|
29
|
-
next self.get_create_from.create(create_data)
|
30
|
-
end
|
28
|
+
ActiveRecord::Base.transaction { self.get_create_from.create(create_data) }
|
31
29
|
end
|
32
30
|
end
|
33
31
|
|
@@ -36,23 +34,21 @@ module RESTFramework::Mixins::BulkUpdateModelMixin
|
|
36
34
|
def update_all
|
37
35
|
records = self.update_all!
|
38
36
|
serialized_records = self.bulk_serialize(records)
|
39
|
-
|
37
|
+
render(api: serialized_records)
|
40
38
|
end
|
41
39
|
|
42
40
|
# Perform the `update` call and return the collection of (possibly) updated records.
|
43
41
|
def update_all!
|
44
42
|
pk = self.class.get_model.primary_key
|
45
|
-
|
43
|
+
data = if params[:_json].is_a?(Array)
|
46
44
|
self.get_create_params(bulk_mode: :update)[:_json].index_by { |r| r[pk] }
|
47
45
|
else
|
48
46
|
create_params = self.get_create_params
|
49
|
-
{create_params[pk] => create_params}
|
47
|
+
{ create_params[pk] => create_params }
|
50
48
|
end
|
51
49
|
|
52
50
|
# Perform bulk update in a transaction.
|
53
|
-
|
54
|
-
next self.get_recordset.update(update_data.keys, update_data.values)
|
55
|
-
end
|
51
|
+
ActiveRecord::Base.transaction { self.get_recordset.update(data.keys, data.values) }
|
56
52
|
end
|
57
53
|
end
|
58
54
|
|
@@ -62,12 +58,11 @@ module RESTFramework::Mixins::BulkDestroyModelMixin
|
|
62
58
|
if params[:_json].is_a?(Array)
|
63
59
|
records = self.destroy_all!
|
64
60
|
serialized_records = self.bulk_serialize(records)
|
65
|
-
return
|
61
|
+
return render(api: serialized_records)
|
66
62
|
end
|
67
63
|
|
68
|
-
|
69
|
-
{message: "Bulk destroy requires an array of primary keys as input."},
|
70
|
-
status: 400,
|
64
|
+
render(
|
65
|
+
api: { message: "Bulk destroy requires an array of primary keys as input." }, status: 400,
|
71
66
|
)
|
72
67
|
end
|
73
68
|
|
@@ -77,9 +72,7 @@ module RESTFramework::Mixins::BulkDestroyModelMixin
|
|
77
72
|
destroy_data = self.request.request_parameters[:_json]
|
78
73
|
|
79
74
|
# Perform bulk destroy in a transaction.
|
80
|
-
|
81
|
-
next self.get_recordset.where(pk => destroy_data).destroy_all
|
82
|
-
end
|
75
|
+
ActiveRecord::Base.transaction { self.get_recordset.where(pk => destroy_data).destroy_all }
|
83
76
|
end
|
84
77
|
end
|
85
78
|
|