rest_framework 1.0.2 → 1.2.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 +31 -26
- data/VERSION +1 -1
- data/app/views/rest_framework/_routes_and_forms.html.erb +2 -5
- data/app/views/rest_framework/routes_and_forms/_html_form.html.erb +1 -1
- data/app/views/rest_framework/routes_and_forms/_raw_form.html.erb +1 -1
- data/lib/rest_framework/controller/bulk.rb +272 -0
- data/lib/rest_framework/controller/crud.rb +65 -0
- data/lib/rest_framework/controller/openapi.rb +252 -0
- data/lib/rest_framework/controller.rb +839 -0
- data/lib/rest_framework/engine.rb +12 -2
- data/lib/rest_framework/errors.rb +53 -4
- data/lib/rest_framework/filters/ordering_filter.rb +0 -1
- data/lib/rest_framework/filters/query_filter.rb +7 -2
- data/lib/rest_framework/filters/search_filter.rb +5 -5
- data/lib/rest_framework/mixins/base_controller_mixin.rb +3 -383
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +27 -68
- data/lib/rest_framework/mixins/model_controller_mixin.rb +60 -807
- data/lib/rest_framework/paginators/page_number_paginator.rb +10 -11
- data/lib/rest_framework/routers.rb +20 -9
- data/lib/rest_framework/serializers/native_serializer.rb +5 -3
- data/lib/rest_framework/utils.rb +24 -6
- data/lib/rest_framework.rb +13 -5
- metadata +6 -7
- data/lib/rest_framework/errors/base_error.rb +0 -5
- data/lib/rest_framework/errors/nil_passed_to_render_api_error.rb +0 -14
- data/lib/rest_framework/errors/unknown_model_error.rb +0 -18
- data/lib/rest_framework/generators/controller_generator.rb +0 -64
- data/lib/rest_framework/generators.rb +0 -4
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
module RESTFramework::Controller
|
|
2
|
+
module ClassMethods
|
|
3
|
+
def openapi_response_content_types
|
|
4
|
+
@openapi_response_content_types ||= [
|
|
5
|
+
"text/html",
|
|
6
|
+
self.serialize_to_json ? "application/json" : nil,
|
|
7
|
+
self.serialize_to_xml ? "application/xml" : nil,
|
|
8
|
+
].compact
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def openapi_request_content_types
|
|
12
|
+
@openapi_request_content_types ||= [
|
|
13
|
+
"application/json",
|
|
14
|
+
"application/x-www-form-urlencoded",
|
|
15
|
+
"multipart/form-data",
|
|
16
|
+
]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def openapi_paths(routes, tag)
|
|
20
|
+
resp_cts = self.openapi_response_content_types
|
|
21
|
+
req_cts = self.openapi_request_content_types
|
|
22
|
+
schema_name = self.openapi_schema_name if self.model
|
|
23
|
+
|
|
24
|
+
routes.group_by { |r| r[:concat_path] }.map { |concat_path, routes|
|
|
25
|
+
[
|
|
26
|
+
concat_path.gsub(/:([0-9A-Za-z_-]+)/, "{\\1}"),
|
|
27
|
+
routes.map { |route|
|
|
28
|
+
metadata = RESTFramework::ROUTE_METADATA[route[:path]] || {}
|
|
29
|
+
summary = metadata.delete(:label).presence || self.label_for(route[:action])
|
|
30
|
+
description = metadata.delete(:description).presence
|
|
31
|
+
extra_action = RESTFramework::EXTRA_ACTION_ROUTES.include?(route[:path])
|
|
32
|
+
error_response = { "$ref" => "#/components/responses/BadRequest" }
|
|
33
|
+
not_found_response = { "$ref" => "#/components/responses/NotFound" }
|
|
34
|
+
spec = { tags: [ tag ], summary: summary, description: description }.compact
|
|
35
|
+
|
|
36
|
+
# All routes should have a successful response.
|
|
37
|
+
success_code = if !extra_action
|
|
38
|
+
if route[:action] == "create"
|
|
39
|
+
201
|
|
40
|
+
elsif route[:action] == "destroy"
|
|
41
|
+
204
|
|
42
|
+
else
|
|
43
|
+
200
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
200
|
|
47
|
+
end
|
|
48
|
+
spec[:responses] = {
|
|
49
|
+
success_code => {
|
|
50
|
+
content: resp_cts.map { |ct|
|
|
51
|
+
[
|
|
52
|
+
ct,
|
|
53
|
+
(self.model && !extra_action && route[:verb] != "OPTIONS") ? {
|
|
54
|
+
schema: { "$ref" => "#/components/schemas/#{schema_name}" },
|
|
55
|
+
} : {},
|
|
56
|
+
]
|
|
57
|
+
}.to_h,
|
|
58
|
+
description: "Success",
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Builtin POST, PUT, PATCH, and DELETE should have a 400 and 404 response.
|
|
63
|
+
if route[:verb].in?([ "POST", "PUT", "PATCH", "DELETE" ]) && !extra_action
|
|
64
|
+
spec[:responses][400] = error_response
|
|
65
|
+
spec[:responses][404] = not_found_response
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# All POST, PUT, PATCH should have a request body.
|
|
69
|
+
if route[:verb].in?([ "POST", "PUT", "PATCH" ])
|
|
70
|
+
spec[:requestBody] ||= {
|
|
71
|
+
content: req_cts.map { |ct|
|
|
72
|
+
[
|
|
73
|
+
ct,
|
|
74
|
+
(self.model && !extra_action) ? {
|
|
75
|
+
schema: { "$ref" => "#/components/schemas/#{schema_name}" },
|
|
76
|
+
} : {},
|
|
77
|
+
]
|
|
78
|
+
}.to_h,
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Add remaining metadata as an extension.
|
|
83
|
+
spec["x-rrf-metadata"] = metadata if metadata.present?
|
|
84
|
+
|
|
85
|
+
next route[:verb].downcase, spec
|
|
86
|
+
}.to_h.merge(
|
|
87
|
+
{
|
|
88
|
+
parameters: routes.first[:route].required_parts.map { |p|
|
|
89
|
+
{
|
|
90
|
+
name: p,
|
|
91
|
+
in: "path",
|
|
92
|
+
required: true,
|
|
93
|
+
schema: { type: "integer" },
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
),
|
|
98
|
+
]
|
|
99
|
+
}.to_h
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def openapi_document(request, route_group_name, routes)
|
|
103
|
+
server = request.base_url + request.original_fullpath.gsub(/\?.*/, "")
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
openapi: "3.1.1",
|
|
107
|
+
info: {
|
|
108
|
+
title: self.get_title,
|
|
109
|
+
description: self.description,
|
|
110
|
+
version: self.version.to_s,
|
|
111
|
+
}.compact,
|
|
112
|
+
servers: [ { url: server } ],
|
|
113
|
+
paths: self.openapi_paths(routes, route_group_name),
|
|
114
|
+
tags: [ { name: route_group_name, description: self.description }.compact ],
|
|
115
|
+
components: {
|
|
116
|
+
schemas: {
|
|
117
|
+
"Error" => {
|
|
118
|
+
type: "object",
|
|
119
|
+
required: [ "message" ],
|
|
120
|
+
properties: {
|
|
121
|
+
message: { type: "string" },
|
|
122
|
+
errors: { type: "object" },
|
|
123
|
+
exception: { type: "string" },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}.merge(self.model ? { self.openapi_schema_name => self.openapi_schema } : {}),
|
|
127
|
+
responses: {
|
|
128
|
+
"BadRequest": {
|
|
129
|
+
description: "Bad Request",
|
|
130
|
+
content: self.openapi_response_content_types.map { |ct|
|
|
131
|
+
[
|
|
132
|
+
ct,
|
|
133
|
+
ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } },
|
|
134
|
+
]
|
|
135
|
+
}.to_h,
|
|
136
|
+
},
|
|
137
|
+
"NotFound": {
|
|
138
|
+
description: "Not Found",
|
|
139
|
+
content: self.openapi_response_content_types.map { |ct|
|
|
140
|
+
[
|
|
141
|
+
ct,
|
|
142
|
+
ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } },
|
|
143
|
+
]
|
|
144
|
+
}.to_h,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}.merge(self.model ? {
|
|
149
|
+
"x-rrf-primary_key" => self.model.primary_key,
|
|
150
|
+
"x-rrf-callbacks" => self._process_action_callbacks.as_json,
|
|
151
|
+
|
|
152
|
+
# While bulk update/destroy are obvious because they create new router endpoints, bulk
|
|
153
|
+
# create overloads the existing collection `POST` endpoint, so we add a special key to the
|
|
154
|
+
# OpenAPI metadata to indicate bulk create is supported.
|
|
155
|
+
"x-rrf-bulk-create": self.bulk,
|
|
156
|
+
} : {}).compact
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Only for model controllers.
|
|
160
|
+
def openapi_schema
|
|
161
|
+
return @openapi_schema if @openapi_schema
|
|
162
|
+
|
|
163
|
+
field_configuration = self.field_configuration
|
|
164
|
+
@openapi_schema = {
|
|
165
|
+
required: field_configuration.select { |_, cfg| cfg[:required] }.keys,
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: field_configuration.map { |f, cfg|
|
|
168
|
+
v = { title: cfg[:label] }
|
|
169
|
+
|
|
170
|
+
if cfg[:kind] == "association"
|
|
171
|
+
v[:type] = cfg[:reflection].collection? ? "array" : "object"
|
|
172
|
+
elsif cfg[:kind] == "rich_text"
|
|
173
|
+
v[:type] = "string"
|
|
174
|
+
v[:"x-rrf-rich_text"] = true
|
|
175
|
+
elsif cfg[:kind] == "attachment"
|
|
176
|
+
v[:type] = "string"
|
|
177
|
+
v[:"x-rrf-attachment"] = cfg[:attachment_type]
|
|
178
|
+
else
|
|
179
|
+
v[:type] = cfg[:type]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
v[:readOnly] = true if cfg[:read_only]
|
|
183
|
+
v[:default] = cfg[:default] if cfg.key?(:default)
|
|
184
|
+
|
|
185
|
+
if enum_variants = cfg[:enum_variants]
|
|
186
|
+
v[:enum] = enum_variants.keys
|
|
187
|
+
v[:"x-rrf-enum_variants"] = enum_variants
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
if validators = cfg[:validators]
|
|
191
|
+
v[:"x-rrf-validators"] = validators
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
v[:"x-rrf-kind"] = cfg[:kind] if cfg[:kind]
|
|
195
|
+
|
|
196
|
+
if cfg[:reflection]
|
|
197
|
+
ref = cfg[:reflection]
|
|
198
|
+
v[:"x-rrf-reflection"] = {
|
|
199
|
+
class_name: ref.respond_to?(:class_name) ? ref.class_name : nil,
|
|
200
|
+
foreign_key: ref.respond_to?(:foreign_key) ? ref.foreign_key : nil,
|
|
201
|
+
association_foreign_key: ref.respond_to?(:association_foreign_key) ?
|
|
202
|
+
ref.association_foreign_key : nil,
|
|
203
|
+
association_primary_key: ref.respond_to?(:association_primary_key) ?
|
|
204
|
+
ref.association_primary_key : nil,
|
|
205
|
+
inverse_of: ref.respond_to?(:inverse_of) ? ref.inverse_of&.name : nil,
|
|
206
|
+
join_table: ref.respond_to?(:join_table) ? ref.join_table : nil,
|
|
207
|
+
}.compact
|
|
208
|
+
v[:"x-rrf-association_pk"] = cfg[:association_pk]
|
|
209
|
+
v[:"x-rrf-sub_fields"] = cfg[:sub_fields]
|
|
210
|
+
v[:"x-rrf-sub_fields_metadata"] = cfg[:sub_fields_metadata]
|
|
211
|
+
v[:"x-rrf-id_field"] = cfg[:id_field]
|
|
212
|
+
v[:"x-rrf-nested_attributes_options"] = cfg[:nested_attributes_options]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
next [ f, v ]
|
|
216
|
+
}.to_h,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@openapi_schema
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Only for model controllers.
|
|
223
|
+
def openapi_schema_name
|
|
224
|
+
@openapi_schema_name ||= self.name.chomp("Controller").gsub("::", ".")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def openapi_document
|
|
229
|
+
first, *rest = self.route_groups.to_a
|
|
230
|
+
document = self.class.openapi_document(request, *first)
|
|
231
|
+
|
|
232
|
+
if self.class.openapi_include_children
|
|
233
|
+
rest.each do |route_group_name, routes|
|
|
234
|
+
controller = "#{routes.first[:route].defaults[:controller]}_controller".camelize.constantize
|
|
235
|
+
child_document = controller.openapi_document(request, route_group_name, routes)
|
|
236
|
+
|
|
237
|
+
# Merge child paths and tags into the parent document.
|
|
238
|
+
document[:paths].merge!(child_document[:paths])
|
|
239
|
+
document[:tags] += child_document[:tags]
|
|
240
|
+
|
|
241
|
+
# If the child document has schemas, merge them into the parent document.
|
|
242
|
+
if schemas = child_document.dig(:components, :schemas) # rubocop:disable Style/Next
|
|
243
|
+
document[:components] ||= {}
|
|
244
|
+
document[:components][:schemas] ||= {}
|
|
245
|
+
document[:components][:schemas].merge!(schemas)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
document
|
|
251
|
+
end
|
|
252
|
+
end
|